package com.hyt.it.ogt.kq.service.gov.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Maps;
import com.hyt.common.apiInfor.ApiResponse;
import com.hyt.core.base.BaseEntity;
import com.hyt.core.base.BaseServiceImpl;
import com.hyt.core.util.DateUtils;
import com.hyt.core.util.FastJsonUtil;
import com.hyt.core.util.UUIDUtils;
import com.hyt.it.ogt.kq.common.config.ConfigManager;
import com.hyt.it.ogt.kq.common.config.exception.KqException;
import com.hyt.it.ogt.kq.common.gov.constant.*;
import com.hyt.it.ogt.kq.common.gov.enums.FileTypeEnum;
import com.hyt.it.ogt.kq.common.gov.utils.*;
import com.hyt.it.ogt.kq.service.admission.IExamineeTimesService;
import com.hyt.it.ogt.kq.service.bm.api.gov.KwGovApi;
import com.hyt.it.ogt.kq.service.gov.api.ks.model.CandidateAreaVO;
import com.hyt.it.ogt.kq.service.gov.api.ks.model.NowTestNowCandidateArrangeVO;
import com.hyt.it.ogt.kq.service.gov.api.ks.model.NowTestNowDataVO;
import com.hyt.it.ogt.kq.service.gov.den.ArrangedResultExportUtil;
import com.hyt.it.ogt.kq.service.gov.feign.ks.KsClient;
import com.hyt.it.ogt.kq.service.gov.feign.ks.model.PushTimeRoomCandidateParam;
import com.hyt.it.ogt.kq.service.gov.feign.ks.model.PushTimeRoomCandidateVO;
import com.hyt.it.ogt.kq.service.gov.mapper.BaseDataPackageMapper;
import com.hyt.it.ogt.kq.service.gov.mapper.ExamAreaMapper;
import com.hyt.it.ogt.kq.service.gov.mapper.TimeRoomCandidateMapper;
import com.hyt.it.ogt.kq.service.gov.model.dto.arrange.*;
import com.hyt.it.ogt.kq.service.gov.model.entity.*;
import com.hyt.it.ogt.kq.service.gov.model.excel.ImportTaskCandidateSheet;
import com.hyt.it.ogt.kq.service.gov.model.excel.TimeRoomCandidateXySheet;
import com.hyt.it.ogt.kq.service.gov.model.excel.handler.CustomCellWriteHandler;
import com.hyt.it.ogt.kq.service.gov.model.excel.listener.TaskCandidateListener;
import com.hyt.it.ogt.kq.service.gov.model.excel.listener.TimeRoomCandidateXyListener;
import com.hyt.it.ogt.kq.service.gov.model.param.ArrangedEditParam;
import com.hyt.it.ogt.kq.service.gov.model.param.DataLockUpdateParam;
import com.hyt.it.ogt.kq.service.gov.model.param.ManualArrangeParam;
import com.hyt.it.ogt.kq.service.gov.model.param.arrange.ArrangeResultByRoomParam;
import com.hyt.it.ogt.kq.service.gov.model.param.arrange.ArrangeResultBySubjectParam;
import com.hyt.it.ogt.kq.service.gov.model.param.arrange.ArrangeResultByTimeParam;
import com.hyt.it.ogt.kq.service.gov.model.vo.*;
import com.hyt.it.ogt.kq.service.gov.model.vo.arrange.ArrangeResultByRoomVO;
import com.hyt.it.ogt.kq.service.gov.model.vo.arrange.ArrangeResultBySubjectVO;
import com.hyt.it.ogt.kq.service.gov.model.vo.arrange.ArrangeResultByTimeVO;
import com.hyt.it.ogt.kq.service.gov.model.vo.examiner.TimeRoomParamVO;
import com.hyt.it.ogt.kq.service.gov.model.vo.tempinterface.UnifiedAdmissionNumberParamVO;
import com.hyt.it.ogt.kq.service.gov.roomdata.model.gce.GceCandidate;
import com.hyt.it.ogt.kq.service.gov.roomdata.model.gce.GceSeat;
import com.hyt.it.ogt.kq.service.gov.roomdata.model.obt.ObtCandidate;
import com.hyt.it.ogt.kq.service.gov.roomdata.model.obt.Seat;
import com.hyt.it.ogt.kq.service.gov.service.*;
import com.hyt.it.ogt.kq.service.gov.service.obs.CloudObsService;
import com.hyt.it.ogt.kq.service.govData.service.kz.IPushTimeRoomCandidateService;
import com.hyt.it.ogt.kq.service.model.entity.Candidate;
import com.hyt.it.ogt.kq.service.model.entity.CandidateSubject;
import com.hyt.it.ogt.kq.service.model.entity.Subject;
import com.hyt.it.ogt.kq.service.model.entity.admission.ExamineeTimes;
import com.hyt.it.ogt.kq.service.model.vo.wechat.PushExamNotifyDTO;
import com.hyt.it.ogt.kq.service.model.vo.wechat.WeChatCandidateArrangeDetailVO;
import com.hyt.it.ogt.kq.service.model.vo.wechat.WeChatCandidateArrangeVO;
import com.hyt.model.PageParam;
import com.hyt.progress.service.ProgressManager;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.hyt.it.ogt.kq.common.gov.enums.CacheKeyEnum.ARRANGE_CANDIDATE_KEY;

/**
 * <p>
 * 考生编排信息表 服务实现类
 * </p>
 *
 * @author huangyh
 * @since 2021-04-08
 */
@Slf4j
@Service
public class TimeRoomCandidateServiceImpl extends BaseServiceImpl<TimeRoomCandidateMapper, TimeRoomCandidate>
        implements ITimeRoomCandidateService {

    @Resource
    private ITaskAreaService taskAreaService;

    @Resource
    private TimeRoomCandidateMapper timeRoomCandidateMapper;

    @Resource
    private ITimeSubjectService timeSubjectService;

    @Resource
    private ITimeRoomCandidateService timeRoomCandidateService;

    @Resource
    private ITimeRoomService iTimeRoomService;

    @Resource
    private ExamAreaMapper examAreaMapper;

    @Resource
    private ITimeRoomParamService timeRoomParamService;

    @Resource
    private BaseDataPackageMapper baseDataPackageMapper;

    @Resource
    private ConfigManager configManager;

    @Resource
    private ITaskService iTaskService;

    @Resource
    private ICandidateSubjectService iCandidateSubjectService;

    @Resource
    private ICandidateService iCandidateService;

    @Resource
    private ITaskRoomService iTaskRoomService;

    @Resource
    private ArrangedResultExportUtil arrangedResultExportUtil;

    @Resource
    private ISysFileService iSysFileService;

    @Resource
    private CloudObsService cloudObsService;

    @Resource
    private ITimeSubjectService iTimeSubjectService;

    @Resource
    private ITaskPlaceService iTaskPlaceService;

    @Resource
    private KsClient ksClient;

    @Resource
    private KwGovApi bmClient;

    @Resource
    private IExamAgreementService iExamAgreementService;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private IExaminerService iExaminerService;

    @Resource
    private ITimeRoomParamService iTimeRoomParamService;

    @Resource
    private ISubjectService iSubjectService;

    @Resource
    private ITimeService iTimeService;

    @Resource
    private ICustomArrangeService iCustomArrangeService;

    @Resource
    private ITaskAreaService iTaskAreaService;
    @Resource
    private TimeRoomExaminerCacheService timeRoomExaminerCacheService;
    @Resource
    private IExamineeTimesService iExamineeTimesService;
    @Resource
    private IPushTimeRoomCandidateService iPushTimeRoomCandidateService;
    @Resource
    private IProctorService iProctorService;
    @Resource
    private IArrangeService iArrangeService;

    public static final int DEFAULT_TOTAL = 100000;

    /**
     * 考生编排
     * 
     * @param timeRoomParamList 编排参数
     * @param taskId            考试任务id
     * @param deleteStatus      是否删除数据重新编排
     * @throws KqException 业务异常
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void candidateArrange(List<TimeRoomParamVO> timeRoomParamList, String taskId, boolean deleteStatus)
            throws Exception {

        Task task = iTaskService.getTaskById(taskId);

        // 0.1 锁定状态验证
        iTaskService.isLockDataThrowException(task);

        // 1.考试任务绑定报名的情况下，需要校验科目数据
        iCandidateSubjectService.checkCandidateSubjectUpdateSubjectCodeOrIdNumber(task);

        // 2.获取编排参数，并且格式化
        Map<String, String> paramMap = timeRoomParamService.getTimeRoomParamMap(timeRoomParamList, taskId);

        String roomType = paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_TYPE.getCode());

        // 3.是否删除数据重新编排，如果是编排页面，则删除，如果是在考生未编排的列表编排，则不用删除
        if (deleteStatus) {

            // 3.2 重新编排的情况下，更新编排参数
            timeRoomParamService.updateTaskTimeRoomParam(timeRoomParamList);
        }

        // 编排参数校验
        this.verifyArrangeParam(paramMap);

        // 获取考试任务考试类型
        String taskExamType = task.getExamType();

        // 根据任务考试类型确定编排方式
        if (TaskExamTypeEnum.ONLINE.getCode().equals(taskExamType)) {
            if (String.valueOf(ArrangeParamCodeEnum.ARRANGE_ROOM_TYPE_VIRTUAL.getCode()).equals(roomType)) {
                // 线上模式，虚拟考场
                this.onlineArrange(paramMap, taskId, deleteStatus);
            } else if (String.valueOf(ArrangeParamCodeEnum.ARRANGE_ROOM_TYPE_PHYSICAL.getCode()).equals(roomType)) {
                // 线上模式，实体考场
                this.offlineArrange(paramMap, taskId, deleteStatus);
            } else if (String.valueOf(ArrangeParamCodeEnum.ARRANGE_ROOM_TYPE_CUSTOM.getCode()).equals(roomType)) {
                // 线上模式，自定义考场
                this.customArrange(paramMap, taskId);
            }
        } else if (TaskExamTypeEnum.OFFLINE.getCode().equals(taskExamType)) {
            // 线下模式，线上模式实体考场使用此模式
            this.offlineArrange(paramMap, taskId, deleteStatus);
        }
        // 清理监控老师相关的缓存
        timeRoomExaminerCacheService.clearUserProctorCache(null, taskId);
    }

    /**
     * 自定义考场编排
     * 
     * @param arrangeParamMap 编排参数
     * @param taskId          考试任务id
     */
    private void customArrange(Map<String, String> arrangeParamMap, String taskId) {
        // 删除现有编排数据
        this.removeCustomArrange(taskId);
        // 获取自定义编排数据
        List<CustomArrange> customArrangeList = this.getCustomArrange(taskId);
        // 获取自定义区域数据
        List<TaskArea> taskAreaList = this.getCustomArea(taskId);
        // 获取自定义考点数据
        TaskPlace taskPlace = this.getCustomPlace(taskId, taskAreaList);
        // 根据自定义编排数据中的考场编号，生成考场数据
        List<TaskRoom> taskRoomList = this.generateCustomRoom(taskId, taskPlace, customArrangeList);
        // 根据自定义编排数据中的批次 和 生成的考场数据 生成场次数据
        List<TimeRoom> timeRoomList = this.generateCustomTimeRoom(taskId, customArrangeList, taskRoomList);
        // 根据生成的考场数据、场次数据、以及自定义准考证号（如果有）生成编排数据
        List<TimeRoomCandidate> timeRoomCandidateList = this.generateCustomTimeRoomCandidate(taskId, taskRoomList,
                timeRoomList, customArrangeList, arrangeParamMap, taskPlace);
        // 保存自定义编排数据
        this.saveCustomArrange(taskRoomList, timeRoomList, timeRoomCandidateList);
    }

    /**
     * 删除自定义编排数据
     * 
     * @param taskId
     */
    private void removeCustomArrange(String taskId) {
        // 删除自定义考场数据
        iTaskRoomService.deleteAllByTaskId(taskId);
        // 删除自定义场次数据
        iTimeRoomService.deleteAllByTaskId(taskId);
        // 删除编排数据
        this.deleteAllArranged(taskId);
    }

    /**
     * 获取自定义编排数据
     * 
     * @param taskId
     * @return
     */
    @Override
    public List<CustomArrange> getCustomArrange(String taskId) {
        // 获取自定义编排数据
        List<CustomArrange> customArrangeList = iCustomArrangeService.listByTaskId(taskId);
        // 筛选出带有自定义编排信息的数据
        List<CustomArrange> hasArrangeList = customArrangeList.stream()
                .filter(customArrange -> StringUtils.isNotBlank(customArrange.getTimeId())
                        && ObjectUtils.isNotEmpty(customArrange.getRoomCode()))
                .collect(Collectors.toList());
        // 校验是否存在自定义场次数据
        if (CollectionUtils.isEmpty(hasArrangeList)) {
            // 当前考试任务暂无自定义考场数据，请完善自定义考场数据，或使用其他编排规则
            throw new KqException(ResponseCode.CUSTOM_ARRANGE_NOT_FOUND.getCode(),
                    ResponseCode.CUSTOM_ARRANGE_NOT_FOUND.getMsg());
        }
        return hasArrangeList;
    }

    /**
     * 获取自定义区域数据
     * 
     * @param taskId
     * @return
     */
    private List<TaskArea> getCustomArea(String taskId) {
        // 获取自定义区域数据
        List<TaskArea> taskAreaList = iTaskAreaService.getCustomArea(taskId);
        // 若无自定义区域数据，则生成自定义区域数据
        if (CollectionUtils.isEmpty(taskAreaList)) {
            return this.generateCustomArea(taskId);
        }
        return taskAreaList;
    }

    /**
     * 生成自定义区域数据
     * 
     * @param taskId
     * @return
     */
    private List<TaskArea> generateCustomArea(String taskId) {
        // 生成自定义区域数据
        List<TaskArea> taskAreaList = new ArrayList<>();
        // 自定义国
        TaskArea country = new TaskArea();
        country.setTaskId(taskId)
                .setAreaId("100000")
                .setName("自定义国")
                .setType(String.valueOf(CodeEnum.AREA_TYPE_NATION.getCode()))
                .setFunction(CodeEnum.AREA_FUNCTION_CUSTOM.getCode());
        taskAreaList.add(country);
        // 自定义省
        TaskArea province = new TaskArea();
        province.setTaskId(taskId)
                .setAreaId("100001")
                .setParentId("100000")
                .setParentIds("100000")
                .setName("自定义省")
                .setType(String.valueOf(CodeEnum.AREA_TYPE_PROVINCE.getCode()))
                .setFunction(CodeEnum.AREA_FUNCTION_CUSTOM.getCode());
        taskAreaList.add(province);
        // 自定义市
        TaskArea city = new TaskArea();
        city.setTaskId(taskId)
                .setAreaId("100011")
                .setParentId("100001")
                .setParentIds("100000,100001")
                .setName("自定义市")
                .setType(String.valueOf(CodeEnum.AREA_TYPE_CITY.getCode()))
                .setFunction(CodeEnum.AREA_FUNCTION_CUSTOM.getCode());
        taskAreaList.add(city);
        // 自定义区县
        TaskArea district = new TaskArea();
        district.setTaskId(taskId)
                .setAreaId("100111")
                .setParentId("100011")
                .setParentIds("100000,100001,100011")
                .setName("自定义区县")
                .setType(String.valueOf(CodeEnum.AREA_TYPE_DISTRICT.getCode()))
                .setFunction(CodeEnum.AREA_FUNCTION_CUSTOM.getCode());
        taskAreaList.add(district);
        if (!iTaskAreaService.saveBatch(taskAreaList)) {
            // 保存自定义区域数据失败
            throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                    "保存自定义区域数据失败");
        }
        return taskAreaList;
    }

    /**
     * 获取自定义考点数据
     * 
     * @param taskId
     * @return
     */
    private TaskPlace getCustomPlace(String taskId, List<TaskArea> taskAreaList) {
        // 获取自定义考点数据
        TaskPlace taskPlace = iTaskPlaceService.oneCustomPlace(taskId);
        // 若无自定义考点数据，则生成自定义考点数据
        return Optional.ofNullable(taskPlace)
                .orElseGet(() -> this.generateCustomPlace(taskId, taskAreaList));
    }

    /**
     * 生成获取自定义考点数据
     * 
     * @param taskId
     * @return
     */
    private TaskPlace generateCustomPlace(String taskId, List<TaskArea> taskAreaList) {
        // 获取省、市、区县编号数据
        String provinceCode = this.getAreaCodeByType(CodeEnum.AREA_TYPE_PROVINCE.getCode(), taskAreaList);
        String cityCode = this.getAreaCodeByType(CodeEnum.AREA_TYPE_CITY.getCode(), taskAreaList);
        String districtCode = this.getAreaCodeByType(CodeEnum.AREA_TYPE_DISTRICT.getCode(), taskAreaList);
        // 生成自定义考点
        TaskPlace taskPlace = new TaskPlace();
        taskPlace.setTaskId(taskId)
                .setName("自定义考点")
                .setProvince(provinceCode)
                .setCity(cityCode)
                .setDistrict(districtCode)
                .setFunction(CodeEnum.PLACE_FUNCTION_CUSTOM.getCode());
        if (!iTaskPlaceService.save(taskPlace)) {
            // 保存自定义考点数据失败
            throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                    "保存自定义考点数据失败");
        }
        return taskPlace;
    }

    /**
     * 根据区域类型获取区域编号
     * 
     * @param type         区域类型
     * @param taskAreaList 区域数据
     * @return 区域编号
     */
    private String getAreaCodeByType(Integer type, List<TaskArea> taskAreaList) {
        return taskAreaList.stream()
                .filter(taskArea -> String.valueOf(type).equals(taskArea.getType()))
                .findFirst()
                .map(TaskArea::getAreaId)
                .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                        "获取自定义区域编号错误"));
    }

    /**
     * 根据自定义编排数据中的考场编号，生成考场数据
     * 
     * @param taskId
     * @param taskPlace
     * @param customArrangeList
     * @return
     */
    private List<TaskRoom> generateCustomRoom(String taskId, TaskPlace taskPlace,
            List<CustomArrange> customArrangeList) {
        // 筛选出所有考场编号
        List<Integer> roomCodeList = customArrangeList.stream()
                .map(CustomArrange::getRoomCode)
                .filter(ObjectUtils::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        // 根据自定义考点、自定义考场编号 生成自定义考场
        return roomCodeList.stream().map(roomCode -> {
            TaskRoom taskRoom = new TaskRoom();
            taskRoom.setTaskId(taskId)
                    .setPlaceId(taskPlace.getId())
                    .setCode(roomCode)
                    .setName("考场" + roomCode)
                    .setFunction(CodeEnum.ROOM_FUNCTION_CUSTOM.getCode())
                    .setId(UUIDUtils.newSortUUID());
            return taskRoom;
        }).collect(Collectors.toList());
    }

    /**
     * 根据自定义编排数据中的批次 和 生成的考场数据 生成场次数据
     * 
     * @param customArrangeList
     * @param taskRoomList
     * @return
     */
    private List<TimeRoom> generateCustomTimeRoom(String taskId, List<CustomArrange> customArrangeList,
            List<TaskRoom> taskRoomList) {
        // 根据自定义编排数据筛选出所有场次
        List<String> timeIdRoomCodeList = customArrangeList.stream()
                .map(customArrange -> customArrange.getTimeId() + "," + customArrange.getRoomCode())
                .distinct()
                .collect(Collectors.toList());
        return timeIdRoomCodeList.stream().map(timeIdRoomCode -> {
            // 获取批次id
            String timeId = StrUtil.subBefore(timeIdRoomCode, ",", true);
            // 获取考场编号
            Integer roomCode = new Integer(StrUtil.subAfter(timeIdRoomCode, ",", true));
            // 获取考场id
            String roomId = taskRoomList.stream()
                    .filter(taskRoom -> taskRoom.getCode().equals(roomCode))
                    .findFirst()
                    .map(TaskRoom::getId)
                    .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                            "生成场次数据时，获取考场id失败"));
            TimeRoom timeRoom = new TimeRoom();
            timeRoom.setTaskId(taskId)
                    .setTimeId(timeId)
                    .setRoomId(roomId)
                    .setId(UUIDUtils.newSortUUID());
            return timeRoom;
        }).collect(Collectors.toList());
    }

    /**
     * 根据生成的考场数据、场次数据、以及自定义准考证号（如果有）生成编排数据
     * 
     * @param taskId
     * @param taskRoomList
     * @param timeRoomList
     * @param customArrangeList
     * @param arrangeParamMap
     * @return
     */
    private List<TimeRoomCandidate> generateCustomTimeRoomCandidate(String taskId, List<TaskRoom> taskRoomList,
            List<TimeRoom> timeRoomList, List<CustomArrange> customArrangeList,
            Map<String, String> arrangeParamMap, TaskPlace taskPlace) {
        // 获取批次科目数据
        List<TimeSubject> timeSubjectList = iTimeSubjectService.listByTaskId(taskId);
        // 获取考生科目数据
        List<CandidateSubject> candidateSubjectList = iCandidateSubjectService.listByTaskId(taskId);
        // 生成编排数据
        List<TimeRoomCandidate> timeRoomCandidateList = new ArrayList<>();
        customArrangeList.forEach(customArrange -> {
            // 根据批次id获取批次科目数据
            String timeId = customArrange.getTimeId();
            List<TimeSubject> timeSubjects = timeSubjectList.stream()
                    .filter(timeSubject -> timeSubject.getTimeId().equals(timeId))
                    .collect(Collectors.toList());
            // 根据批次科目数据生成编排数据
            List<TimeRoomCandidate> list = timeSubjects.stream().map(timeSubject -> {
                // 根据考场编号获取考场id
                Integer roomCode = customArrange.getRoomCode();
                String roomId = taskRoomList.stream()
                        .filter(taskRoom -> taskRoom.getCode().equals(roomCode))
                        .findFirst()
                        .map(TaskRoom::getId)
                        .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                                "生成自定义编排数据时，根据考场编号获取考场id失败"));
                // 根据考场id、批次id获取场次id
                String timeRoomId = timeRoomList.stream()
                        .filter(timeRoom -> timeRoom.getRoomId().equals(roomId)
                                && timeRoom.getTimeId().equals(customArrange.getTimeId()))
                        .findFirst()
                        .map(TimeRoom::getId)
                        .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                                "生成自定义编排数据时，根据考场id、批次id获取场次id失败"));
                // 根据科目id、考生id获取考生科目id
                String candidateSubjectId = candidateSubjectList.stream()
                        .filter(candidateSubject -> candidateSubject.getSubjectId().equals(timeSubject.getSubjectId())
                                && candidateSubject.getCandidateId().equals(customArrange.getCandidateId()))
                        .findFirst()
                        .map(CandidateSubject::getId)
                        .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                                "生成自定义编排数据时，根据科目id、考生id获取考生科目id失败"));
                // 组装编排数据
                TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
                // 考试任务id
                timeRoomCandidate.setTaskId(taskId);
                // 考生id
                timeRoomCandidate.setCandidateId(customArrange.getCandidateId());
                // 场次id
                timeRoomCandidate.setTimeRoomId(timeRoomId);
                // 批次id
                timeRoomCandidate.setTimeId(customArrange.getTimeId());
                // 考场id
                timeRoomCandidate.setRoomId(roomId);
                // 科目id
                timeRoomCandidate.setSubjectId(timeSubject.getSubjectId());
                // 考生科目id
                timeRoomCandidate.setCandidateSubjectId(candidateSubjectId);
                return timeRoomCandidate;
            }).collect(Collectors.toList());
            timeRoomCandidateList.addAll(list);
        });
        // 生成准考证号
        this.generateCustomAdmissionNumber(taskId, arrangeParamMap, customArrangeList, timeRoomCandidateList,
                taskPlace);
        return timeRoomCandidateList;
    }

    /**
     * 生成准考证号
     * 
     * @param taskId
     * @param arrangeParamMap
     * @param timeRoomCandidateList
     */
    private void generateCustomAdmissionNumber(String taskId, Map<String, String> arrangeParamMap,
            List<CustomArrange> customArrangeList,
            List<TimeRoomCandidate> timeRoomCandidateList, TaskPlace taskPlace) {
        // 获取准考证号生成方式
        Integer admissionType = new Integer(arrangeParamMap.get(StringCodeEnum.ARRANGE_PARAM_ADMISSION_TYPE.getCode()));
        if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
            // 使用自定义准考证号
            this.generateCustomAdmissionNumber(customArrangeList, timeRoomCandidateList);
        } else {
            // 使用准考证生成规则生成准考证
            TaskPlaceVO taskPlaceVO = new TaskPlaceVO();
            taskPlaceVO.setProvince(taskPlace.getProvince())
                    .setCity(taskPlace.getCity())
                    .setDistrict(taskPlace.getDistrict());
            Map<String, String> candidateIdAdmissionNumberMap = new HashMap<>();
            timeRoomCandidateList.forEach(timeRoomCandidate -> {
                String candidateId = timeRoomCandidate.getCandidateId();
                String admissionNumber = candidateIdAdmissionNumberMap.get(candidateId);
                if (StringUtils.isBlank(admissionNumber)) {
                    admissionNumber = this.getAdmissionNumber(taskId, arrangeParamMap, taskPlaceVO);
                    candidateIdAdmissionNumberMap.put(candidateId, admissionNumber);
                }
                timeRoomCandidate.setAdmissionNum(admissionNumber);
            });
        }
    }

    /**
     * 使用自定义准考证号为编排考生生成准考证号
     * 
     * @param customArrangeList
     * @param timeRoomCandidateList
     */
    private void generateCustomAdmissionNumber(List<CustomArrange> customArrangeList,
            List<TimeRoomCandidate> timeRoomCandidateList) {
        // 校验是否有自定义准考证号
        List<TimeRoomCandidate> noneAdmissionNumberList = timeRoomCandidateList.stream().filter(timeRoomCandidate -> {
            String admissionNumber = customArrangeList.stream()
                    .filter(customArrange -> customArrange.getCandidateId().equals(timeRoomCandidate.getCandidateId())
                            && customArrange.getTimeId().equals(timeRoomCandidate.getTimeId()))
                    .findFirst()
                    .map(CustomArrange::getAdmissionNumber)
                    .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                            "使用自定义准考证号为编排考生生成准考证号时，校验是否有自定义准考证号失败"));
            return StringUtils.isBlank(admissionNumber);
        }).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(noneAdmissionNumberList)) {
            // 当前考试任务存在暂无自定义准考证号的考生，请录入自定义准考证号，或使用其他准考证号生成方式
            throw new KqException(ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getCode(),
                    ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getMsg());
        }
        timeRoomCandidateList.forEach(timeRoomCandidate -> {
            String admissionNumber = customArrangeList.stream()
                    .filter(customArrange -> customArrange.getCandidateId().equals(timeRoomCandidate.getCandidateId())
                            && customArrange.getTimeId().equals(timeRoomCandidate.getTimeId()))
                    .findFirst()
                    .map(CustomArrange::getAdmissionNumber)
                    .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                            "使用自定义准考证号为编排考生生成准考证号时，获取准考证号失败"));
            timeRoomCandidate.setAdmissionNum(admissionNumber);
        });
    }

    /**
     * 保存自定义编排数据
     * 
     * @param taskRoomList
     * @param timeRoomList
     * @param timeRoomCandidateList
     */
    private void saveCustomArrange(List<TaskRoom> taskRoomList, List<TimeRoom> timeRoomList,
            List<TimeRoomCandidate> timeRoomCandidateList) {
        // 保存自定义考场数据
        if (!iTaskRoomService.saveBatch(taskRoomList)) {
            // 保存自定义考场数据失败
            throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                    "保存自定义考场数据失败");
        }
        // 保存自定义场次数据
        if (!iTimeRoomService.saveBatch(timeRoomList)) {
            // 保存自定义场次数据失败
            throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                    "保存自定义场次数据失败");
        }
        // 保存自定义编排数据
        if (!this.saveBatch(timeRoomCandidateList)) {
            // 保存自定义编排数据失败
            throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                    "保存自定义编排数据失败");
        }
    }

    /**
     * 编排参数校验
     * 
     * @param paramMap 编排参数
     */
    private void verifyArrangeParam(Map<String, String> paramMap) {
        Integer admissionType = Integer
                .parseInt(String.valueOf(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ADMISSION_TYPE.getCode())));
        if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_GROUP.getCode())) {
            // 准考证号生成方式：自定义组合方式
            if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_RANDOM.getCode())
                    .equals(StringCodeEnum.ARRANGE_STATUS_ON.getCode())) {
                // 启用随机序号组合方式，校验随机序号位数
                String randomDigit = paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_RANDOM_DIGIT.getCode());
                NumberUtil.isIntegerVerify(randomDigit,
                        ResponseCode.RANDOM_DIGIT_MUST_BE_INTEGER.getCode(),
                        ResponseCode.RANDOM_DIGIT_MUST_BE_INTEGER.getMsg());
                NumberUtil.isGreaterThanZeroVerify(randomDigit,
                        ResponseCode.RANDOM_DIGIT_MUST_BE_GREATER_THAN_ZERO.getCode(),
                        ResponseCode.RANDOM_DIGIT_MUST_BE_GREATER_THAN_ZERO.getMsg());
            }
            if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_AUTO.getCode())
                    .equals(StringCodeEnum.ARRANGE_STATUS_ON.getCode())) {
                // 启用自增序号组合方式，自增随机序号位数
                String groupAutoDigit = paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_AUTO_DIGIT.getCode());
                NumberUtil.isIntegerVerify(groupAutoDigit,
                        ResponseCode.AUTO_DIGIT_MUST_BE_INTEGER.getCode(),
                        ResponseCode.AUTO_DIGIT_MUST_BE_INTEGER.getMsg());
                NumberUtil.isGreaterThanZeroVerify(groupAutoDigit,
                        ResponseCode.AUTO_DIGIT_MUST_BE_GREATER_THAN_ZERO.getCode(),
                        ResponseCode.AUTO_DIGIT_MUST_BE_GREATER_THAN_ZERO.getMsg());
            }
        } else if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_MANUAL.getCode())) {
            // 准考证号生成方式：自定义手动方式
            String manual = paramMap.get(StringCodeEnum.ARRANGE_PARAM_MANUAL.getCode());
            if (StringUtils.isEmpty(manual)) {
                throw new KqException(ResponseCode.MANUAL_MUST_BE_NOT_NULL.getCode(),
                        ResponseCode.MANUAL_MUST_BE_NOT_NULL.getMsg());
            }
            String manualIncreaseDigit = paramMap.get(StringCodeEnum.ARRANGE_PARAM_MANUAL_INCREASE_DIGIT.getCode());
            NumberUtil.isIntegerVerify(manualIncreaseDigit,
                    ResponseCode.AUTO_DIGIT_MUST_BE_INTEGER.getCode(),
                    ResponseCode.AUTO_DIGIT_MUST_BE_INTEGER.getMsg());
            NumberUtil.isGreaterThanZeroVerify(manualIncreaseDigit,
                    ResponseCode.AUTO_DIGIT_MUST_BE_GREATER_THAN_ZERO.getCode(),
                    ResponseCode.AUTO_DIGIT_MUST_BE_GREATER_THAN_ZERO.getMsg());
        } else if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_RANDOM.getCode())) {
            // 准考证号生成方式：随机方式
            // 校验随机参数
            String randomParam = paramMap.get(StringCodeEnum.ARRANGE_PARAM_RANDOM_PARAM.getCode());
            if (StringUtils.isBlank(randomParam)) {
                throw new KqException(ResponseCode.ADMISSION_RANDOM_PARAM_MUST_BE_NOT_NULL.getCode(),
                        "随机参数不能为空");
            }
            if (String.valueOf(ArrangeParamCodeEnum.NULL_SELECT.getCode()).equals(randomParam)) {
                throw new KqException(ResponseCode.ADMISSION_RANDOM_PARAM_MUST_BE_NOT_NULL.getCode(),
                        "至少选择1个随机参数");
            }
            // 校验准考证位数
            String randomDigit = paramMap.get(StringCodeEnum.ARRANGE_PARAM_RANDOM_DIGIT.getCode());
            NumberUtil.isIntegerVerify(randomDigit,
                    ResponseCode.ADMISSION_DIGIT_MUST_BE_INTEGER.getCode(),
                    ResponseCode.ADMISSION_DIGIT_MUST_BE_INTEGER.getMsg());
            NumberUtil.isGreaterThanZeroVerify(randomDigit,
                    ResponseCode.ADMISSION_DIGIT_MUST_BE_GREATER_THAN_ZERO.getCode(),
                    ResponseCode.ADMISSION_DIGIT_MUST_BE_GREATER_THAN_ZERO.getMsg());
        }
    }

    /**
     * 简单线上机考编排（仅支持1个批次考1个科目）
     * 
     * @param paramMap     编排参数
     * @param taskId       考试任务id
     * @param deleteStatus 是否删除数据重新编排
     */
    private void simpleOnlineArrange(Map<String, String> paramMap, String taskId, boolean deleteStatus) {
        // 1。如果是重新编排，则删除已有编排数据
        String roomType = paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_TYPE.getCode());
        if (deleteStatus) {
            this.deleteArrangedByOnline(taskId);
        }
        // 获取待编排的【考生-科目】数据
        List<CandidateSubjectDTO> candidateSubjects = iCandidateSubjectService.getNoArrangeCandidateByOnLine(taskId);
        if (candidateSubjects == null || candidateSubjects.isEmpty()) {
            // 无考生，不允许编排
            throw new KqException(ResponseCode.ARRANGE_NO_CANDIDATE_ERROR.getCode(),
                    ResponseCode.ARRANGE_NO_CANDIDATE_ERROR.getMsg());
        }

        // 2。组装【批次-考生】数据
        Map<String, List<CandidateSubjectDTO>> timeCandidateMap = this.getTimeCandidateMap(taskId, candidateSubjects);

        // 3。组装【批次-考场】数据，可以使用物理考场，也可以使用虚拟考场
        Map<String, List<TimeRoom>> timeRoomMap = null;
        // if (StringUtils.isBlank(roomType) ||
        // ArrangeParamCodeEnum.ADMISSION_ROOM_TYPE_VIRTUAL.getCode().equals(roomType))
        // {
        // 使用虚拟考场
        timeRoomMap = this.getTimeRoomAndCreate(taskId, timeCandidateMap, paramMap);
        /*
         * } else {
         * //使用物理考场
         * timeRoomMap = iTimeRoomService.getTaskTimeRoomMap(taskId);
         * }
         */

        // 4。开始编排
        List<TimeRoomCandidate> timeRoomCandidateList = new ArrayList<>();

        // 4.1 遍历批次
        timeRoomMap.forEach((timeId, timeRooms) -> {
            List<CandidateSubjectDTO> candidateList = timeCandidateMap.get(timeId);
            // List<TimeRoom> timeRoomList = timeRoomMap.get(timeId);
            // 4.2 遍历考生
            candidateList.stream().forEach(candidateSubjectDTO -> {
                // 按照实际编排人数排序，找出编排人数最少的场次，再进行编排
                TimeRoom timeRoom = timeRooms.stream()
                        .min(Comparator.comparingInt(TimeRoom::getActualCount))
                        .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                                "获取最少考生数场次失败"));

                if (timeRoom.getPlanCount() > timeRoom.getActualCount()) {
                    // 更新实际编排数据
                    timeRoom.setActualCount(timeRoom.getActualCount() + 1);
                    // 组装编排考生数据，不包含准考证号
                    TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
                    timeRoomCandidate.setCandidateId(candidateSubjectDTO.getId())
                            .setTaskId(taskId)
                            .setTimeId(timeId)
                            .setSeatNum("0")
                            .setRoomId(timeRoom.getRoomId())
                            .setSubjectId(candidateSubjectDTO.getSubjectId())
                            .setCandidateSubjectId(candidateSubjectDTO.getCandidateSubjectId())
                            .setId(UUIDUtils.newSortUUID());
                    timeRoomCandidateList.add(timeRoomCandidate);
                    // 当前人编排完了，调出循环，接着下一个考生
                }
            });

            // 5。场次数据更新
            iTimeRoomService.updateBatchById(timeRooms);
        });

        // 6.生成准考证号，并入库
        // 同个考生不同批次，准考证号相同
        TaskPlace taskPlace = iTaskPlaceService.getOneTaskPlace(taskId);
        TaskPlaceVO taskPlaceVO = new TaskPlaceVO();
        taskPlaceVO.setCity(taskPlace.getCity())
                .setProvince(taskPlace.getProvince())
                .setDistrict(taskPlace.getDistrict());

        this.generateAdmissionNumberAndInsert(taskId, paramMap, timeRoomCandidateList, taskPlaceVO);
    }

    /**
     * 获取批次-科目-考上数据
     * 
     * @param taskId
     * @return
     */
    private Map<String, List<CandidateSubjectDTO>> getTimeCandidateMap(String taskId,
            List<CandidateSubjectDTO> candidateSubjects) {
        // 科目考生人数分组--subjectId为key
        Map<String, List<CandidateSubjectDTO>> candidateSubjectGroupMap = candidateSubjects.stream()
                .collect(Collectors.groupingBy(CandidateSubjectDTO::getSubjectId));

        // 2。获取【批次-科目】数据（简单线上机考编排下，1个批次只会对应1个科目，且不同批次不会考相同科目）
        List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList = iTimeSubjectService
                .getTimeSubjectCandidateDTO(taskId);
        if (timeSubjectCandidateDTOList == null || timeSubjectCandidateDTOList.isEmpty()) {
            // 无批次，不允许编排
            throw new KqException(ResponseCode.ARRANGE_NO_TIME_ROOM_ERROR.getCode(),
                    ResponseCode.ARRANGE_NO_TIME_ROOM_ERROR.getMsg());
        }

        // 3.组装【批次-考生】
        Map<String, List<CandidateSubjectDTO>> timeCandidateMap = new HashMap<>();
        timeSubjectCandidateDTOList.stream().forEach(timeSubjectCandidateDTO -> {
            String subjectId = timeSubjectCandidateDTO.getSubjectCandidateDTOList().get(0).getSubjectId();
            timeCandidateMap.put(timeSubjectCandidateDTO.getId(), candidateSubjectGroupMap.get(subjectId));
        });

        return timeCandidateMap;
    }

    /**
     * 获取批次考场数据
     * 如果无场次数据，则创建
     * 如果无考场数据，则创建
     * 
     * @param taskId
     * @param timeCandidateMap
     * @param paramMap
     * @return
     */
    private Map<String, List<TimeRoom>> getTimeRoomAndCreate(String taskId,
            Map<String, List<CandidateSubjectDTO>> timeCandidateMap, Map<String, String> paramMap) {

        // 如果没有考点，则新增考点
        iTaskPlaceService.initVirtualTaskPlaceRandomDistrict(taskId);

        // 计算最大考场人数，并入库
        Integer roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));

        // 生成场次，并返回场次数据
        Map<String, List<TimeRoom>> timeRoomMap = iTimeRoomService.getTaskTimeRoomAndCreate(timeCandidateMap, taskId,
                roomMaxCandidateCount);

        return timeRoomMap;
    }

    /**
     * 创建新的考场，并将剩余未编排的考生安排至新的考场中
     * 
     * @param candidateSubjectDTOList  所有待编排的考生
     * @param timeRoomCandidateDTOList 已编排的批次考场考生数据
     * @param paramMap                 编排参数
     * @param taskId                   考试任务id
     * @param timeSubjectIdMap         批次-科目映射关系（key是批次id，value是科目id）
     * @return 新的考场数据
     */
    private List<TaskRoom> createContinueSimpleOnlineArrangeRoom(List<CandidateSubjectDTO> candidateSubjectDTOList,
            List<TimeRoomCandidateDTO> timeRoomCandidateDTOList,
            Map<String, String> paramMap, String taskId,
            Map<String, String> timeSubjectIdMap) {
        // 获取已编排考生
        List<CandidateSubjectDTO> arrangedCandidateList = new ArrayList<>();
        timeRoomCandidateDTOList
                .forEach(timeRoomCandidate -> arrangedCandidateList.addAll(timeRoomCandidate.getCandidateList()));

        // 获取剩余未编排的考生
        List<CandidateSubjectDTO> unArrangedCandidateList = candidateSubjectDTOList.stream()
                .filter(candidate -> arrangedCandidateList.stream()
                        .noneMatch(arrangedCandidate -> arrangedCandidate.getCandidateSubjectId()
                                .equals(candidate.getCandidateSubjectId())))
                .collect(Collectors.toList());

        List<TaskRoom> newTaskRoomList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(unArrangedCandidateList)) {
            // 获取当前任务下最后一个考场的信息
            TaskRoom lastTaskRoom = iTaskRoomService.lambdaQuery()
                    .eq(TaskRoom::getTaskId, taskId)
                    .eq(TaskRoom::getDelFlag, false)
                    .orderByDesc(TaskRoom::getCode)
                    .last("LIMIT 1")
                    .one();

            // 将剩余未编排考生根据科目分组（String是科目id，Long是剩余未编排的考生数量）
            Map<String, Long> subjectIdUnArrangeCount = unArrangedCandidateList.stream()
                    .collect(Collectors.groupingBy(CandidateSubjectDTO::getSubjectId, Collectors.counting()));

            // 获取剩余未编排考生数的最大值
            int maxUnArrangeCount = subjectIdUnArrangeCount.values()
                    .stream()
                    .max(Comparator.comparingLong(Long::longValue))
                    .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                            ResponseCode.ARRANGE_DATA_EXCEPTION.getMsg()))
                    .intValue();

            // 从编排参数中获取考场最大考生数
            int roomMaxCandidateCount = Integer
                    .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));

            // 根据剩余未编排考生数的最大值，生成新考场数据
            int newRoomCount = maxUnArrangeCount / roomMaxCandidateCount;
            if (maxUnArrangeCount % roomMaxCandidateCount > 0) {
                newRoomCount++;
            }
            for (int i = 0; i < newRoomCount; i++) {
                TaskRoom taskRoom = new TaskRoom();
                taskRoom.setTaskId(taskId)
                        .setPlaceId(lastTaskRoom.getPlaceId())
                        .setCode(lastTaskRoom.getCode() + i + 1)
                        .setName("虚拟考场" + lastTaskRoom.getCode() + i + 1)
                        .setSeatNum(roomMaxCandidateCount)
                        .setAnswerType(1)
                        .setReserve(false)
                        .setRegistered(false)
                        .setClosure(false)
                        .setId(UUIDUtils.newSortUUID());
                newTaskRoomList.add(taskRoom);
            }

            // 根据剩余未编排考生根据科目的分组，生成新的场次数据
            List<TimeRoomCandidateDTO> newTimeRoomCandidateDTO = new ArrayList<>();
            subjectIdUnArrangeCount.forEach((subjectId, unArrangeCount) -> {
                String timeId = null;
                for (Map.Entry<String, String> timeSubjectIdEntry : timeSubjectIdMap.entrySet()) {
                    if (subjectId.equals(timeSubjectIdEntry.getValue())) {
                        timeId = timeSubjectIdEntry.getKey();
                        break;
                    }
                }
                int subjectRoomCount = unArrangeCount.intValue() / roomMaxCandidateCount;
                if (unArrangeCount.intValue() % roomMaxCandidateCount > 0) {
                    subjectRoomCount++;
                }
                for (int i = 0; i < subjectRoomCount; i++) {
                    TimeRoomCandidateDTO timeRoomCandidateDTO = new TimeRoomCandidateDTO();
                    timeRoomCandidateDTO.setTimeId(timeId)
                            .setRoomId(newTaskRoomList.get(i).getId())
                            .setCandidateCount(roomMaxCandidateCount);
                    newTimeRoomCandidateDTO.add(timeRoomCandidateDTO);
                }
            });

            // 将剩余未编排的考生放入新的场次数据中
            loop: for (CandidateSubjectDTO unArrangeCandidate : unArrangedCandidateList) {
                for (TimeRoomCandidateDTO timeRoomCandidateDTO : newTimeRoomCandidateDTO) {
                    String subjectId = timeSubjectIdMap.get(timeRoomCandidateDTO.getTimeId());
                    if (unArrangeCandidate.getSubjectId().equals(subjectId)
                            && timeRoomCandidateDTO.getCandidateList().size() < timeRoomCandidateDTO
                                    .getCandidateCount()) {
                        // 当且仅当考生报考的科目与当前场次绑定的科目相同，且当前场次已分配考生数小于最大考生数时，可将考生分配至当前场次中
                        timeRoomCandidateDTO.getCandidateList().add(unArrangeCandidate);
                        continue loop;
                    }
                }
            }

            // 将新的场次考生数据放入最终继续编排场次考生数据中
            timeRoomCandidateDTOList.addAll(newTimeRoomCandidateDTO);
        }
        return newTaskRoomList;
    }

    /**
     * 获取批次-科目映射关系（key是批次id，value是科目id）
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目】数据
     * @return 批次-科目映射关系
     */
    private Map<String, String> getTimeSubjectIdMap(List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList) {
        return timeSubjectCandidateDTOList.stream()
                .collect(Collectors.toMap(TimeSubjectCandidateDTO::getId,
                        timeSubject -> timeSubject.getSubjectCandidateDTOList()
                                .stream()
                                .findFirst()
                                .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                                        ResponseCode.ARRANGE_DATA_EXCEPTION.getMsg()))
                                .getSubjectId()));
    }

    /**
     * 简单线上编排-继续编排
     * 
     * @param timeRoomDTOList         已编排的场次数据
     * @param candidateSubjectDTOList 待编排的【考生-科目】数据
     * @param timeSubjectIdMap        【批次-科目】映射关系
     */
    private void continueSimpleOnlineArrange(List<TimeRoomCandidateDTO> timeRoomDTOList,
            List<CandidateSubjectDTO> candidateSubjectDTOList,
            Map<String, String> timeSubjectIdMap,
            Map<String, String> paramMap) {
        // 简单线上编排-继续编排情况下，编排参数中的考场最大考生数不可修改
        int roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));

        // 设置场次下可继续编排的考生数
        timeRoomDTOList.forEach(
                timeRoomDTO -> timeRoomDTO.setCandidateCount(roomMaxCandidateCount - timeRoomDTO.getCandidateCount()));

        // 考生放入场次中
        loop: for (CandidateSubjectDTO candidate : candidateSubjectDTOList) {
            for (TimeRoomCandidateDTO timeRoomCandidateDTO : timeRoomDTOList) {
                String subjectId = timeSubjectIdMap.get(timeRoomCandidateDTO.getTimeId());
                if (candidate.getSubjectId().equals(subjectId)
                        && timeRoomCandidateDTO.getCandidateList().size() < timeRoomCandidateDTO.getCandidateCount()) {
                    // 考生报考的科目与批次绑定的科目相同，且场次已分配考生数小于场次可继续编排考生数，即可将考生编排至当前场次下
                    timeRoomCandidateDTO.getCandidateList().add(candidate);
                    // 继续遍历下一位考生
                    continue loop;
                }
            }
        }
    }

    /**
     * 将考生数据放入考场中（简单组装，仅支持1个批次考1个科目）
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目-考生】数据
     * @param taskRoomList                待编排考场数据
     * @param taskId                      考试任务id
     * @param paramMap                    编排参数
     * @return 编排结果(缺少场次id、座位号、准考证号)
     */
    private List<TimeRoomCandidate> simpleAssembleRoomCandidate(
            List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList,
            List<TaskRoom> taskRoomList, String taskId, Map<String, String> paramMap) {
        // 编排结果数据
        List<TimeRoomCandidate> timeRoomCandidateList = new ArrayList<>();

        // 从编排参数中获取考场最大考生数
        int roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));

        // 遍历批次
        timeSubjectCandidateDTOList.forEach(
                timeSubjectCandidateDTO -> timeSubjectCandidateDTO.getSubjectCandidateDTOList().stream().findFirst()
                        .ifPresent(subject -> {
                            List<CandidateSubjectDTO> candidateList = subject.getCandidateList();
                            if (CollectionUtils.isNotEmpty(candidateList)) {
                                // 粗略计算批次所需考场数
                                int timeCandidateCount = candidateList.size();
                                int roomCount = timeCandidateCount / roomMaxCandidateCount;
                                if (timeCandidateCount % roomMaxCandidateCount > 0) {
                                    roomCount++;
                                }
                                // 使用整数均分递归算法，计算批次下各考场的实际考生数
                                List<Integer> roomCandidateParts = new ArrayList<>();
                                MathUtil.averageNumberByRecursive(timeCandidateCount, roomCount, roomMaxCandidateCount,
                                        roomCandidateParts);

                                // 创建批次-考场-考生数据结构
                                List<TimeRoomCandidateDTO> timeRoomCandidateDTOList = new ArrayList<>();
                                for (int i = 0; i < roomCandidateParts.size(); i++) {
                                    TimeRoomCandidateDTO timeRoomCandidateDTO = new TimeRoomCandidateDTO();
                                    String timeId = timeSubjectCandidateDTO.getId();
                                    String roomId = taskRoomList.get(i).getId();
                                    timeRoomCandidateDTO.setTimeId(timeId)
                                            .setRoomId(roomId)
                                            .setCandidateCount(roomCandidateParts.get(i));
                                    timeRoomCandidateDTOList.add(timeRoomCandidateDTO);
                                }

                                // 当前批次下的考生数据放入批次-考场-考生数据结构中
                                loop: for (CandidateSubjectDTO candidate : subject.getCandidateList()) {
                                    // 考生数据放入批次-考场-考生数据结构中
                                    for (TimeRoomCandidateDTO timeRoomCandidateDTO : timeRoomCandidateDTOList) {
                                        // 当前场次下的考生数小于计划安排考生数时可将考生安排到当前场次下
                                        if (timeRoomCandidateDTO.getCandidateList().size() < timeRoomCandidateDTO
                                                .getCandidateCount()) {
                                            timeRoomCandidateDTO.getCandidateList().add(candidate);
                                            // 继续遍历下一位考生
                                            continue loop;
                                        }
                                    }
                                }

                                // 生成编排结果数据
                                timeRoomCandidateDTOList.forEach(timeRoomCandidateDTO -> timeRoomCandidateDTO
                                        .getCandidateList().forEach(candidate -> {
                                            TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
                                            timeRoomCandidate.setCandidateId(candidate.getId())
                                                    .setTaskId(taskId)
                                                    .setTimeId(timeRoomCandidateDTO.getTimeId())
                                                    .setRoomId(timeRoomCandidateDTO.getRoomId())
                                                    .setSubjectId(candidate.getSubjectId())
                                                    .setCandidateSubjectId(candidate.getCandidateSubjectId())
                                                    .setId(UUIDUtils.newSortUUID());
                                            timeRoomCandidateList.add(timeRoomCandidate);
                                        }));
                            }
                        }));
        return timeRoomCandidateList;
    }

    /**
     * 将【考生-科目】数据根据【批次-科目】的对应关系放入批次中，组合成【批次-科目-考生】数据（仅支持1个批次考1个科目）
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目】数据
     */
    private Map<String, List<TimeSubjectCandidateDTO>> simpleAssembleTimeSubjectCandidate(
            List<CandidateSubjectDTO> candidateSubjects,
            List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList, Map<String, String> paramMap) {

        // 科目考生人数分组--subjectId为key
        Map<String, List<CandidateSubjectDTO>> candidateSubjectGroupMap = candidateSubjects.stream()
                .collect(Collectors.groupingBy(CandidateSubjectDTO::getSubjectId));

        // 考生数据放进批次科目数据中
        timeSubjectCandidateDTOList.stream().forEach(timeSubjectCandidateDTO -> {
            // 定义初始值
            timeSubjectCandidateDTO.getSubjectCandidateDTOList().stream().forEach(subjectCandidateDTO -> {
                List<CandidateSubjectDTO> candidateSubjectDTOs = candidateSubjectGroupMap
                        .get(subjectCandidateDTO.getSubjectId());

            });

            // 获取批次最大考生数
            // Integer maxTimeSubjectCandidateCount =
            // Integer.parseInt(paramMap.get(StringCodeEnum.TIME_CANDIDATE_COUNT_MAX_KEY.getCode()));
            // if (timeSubjectCandidateDTO.getCandidateCount().intValue() >
            // maxTimeSubjectCandidateCount.intValue()) {
            //// paramMap.put(StringCodeEnum.TIME_CANDIDATE_COUNT_MAX_KEY.getCode(),String.valueOf(timeSubjectCandidateDTO.getCandidateCount()));
            // }
        });

        // 批次科目分组--批次id为key
        return timeSubjectCandidateDTOList.stream().collect(Collectors.groupingBy(TimeSubjectCandidateDTO::getId));
    }

    /**
     * 线上机考编排
     * 
     * @param paramMap     编排参数
     * @param taskId       考试任务id
     * @param deleteStatus 是否删除数据重新编排
     * @throws KqException 业务异常
     */
    private void onlineArrange(Map<String, String> paramMap, String taskId, boolean deleteStatus) throws Exception {
        // 编排参数校验
        this.verifyOnlineParam(paramMap);
        // 获取编排模式
        // 目前暂时只支持编排整个考试任务，预计后续会支持编排考试任务下指定批次
        // String arrangeMode = paramMap.get("arrangeMode");
        String arrangeMode = "task";
        String cacheKey = ARRANGE_CANDIDATE_KEY.getCacheEnumKey(taskId);
        // 该任务是否正在执行编排(分布式锁)，避免多用户操作同一任务
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(cacheKey, true,
                ARRANGE_CANDIDATE_KEY.getExpireSeconds(), TimeUnit.SECONDS);
        if (lockFlag) {
            try {
                switch (arrangeMode) {
                    // 编排整个考试任务
                    case "task":
                        // this.onlineArrangeByTask(paramMap, taskId, deleteStatus);
                        this.simpleOnlineArrange(paramMap, taskId, deleteStatus);
                        break;
                    // 编排考试任务下指定批次
                    case "time":
                        this.onlineArrangeByTime(paramMap, taskId, deleteStatus);
                        break;
                    default:
                        // 编排模式不存在
                        throw new KqException(ResponseCode.ARRANGE_MODE_NOT_FOUND.getCode(),
                                ResponseCode.ARRANGE_MODE_NOT_FOUND.getMsg());
                }
            } finally {
                redisTemplate.delete(cacheKey);
            }
        } else {
            log.info("# 任务正在编排中，重复操作：{}", taskId);
        }
    }

    /**
     * 线上机考编排参数校验
     * 
     * @param paramMap 编排参数
     */
    private void verifyOnlineParam(Map<String, String> paramMap) {
        // 校验考场最大考生人数
        String roomMaxCandidateCountString = paramMap
                .get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode());
        NumberUtil.isIntegerVerify(roomMaxCandidateCountString,
                ResponseCode.ROOM_MAX_CANDIDATE_COUNT_MUST_BE_INTEGER.getCode(),
                ResponseCode.ROOM_MAX_CANDIDATE_COUNT_MUST_BE_INTEGER.getMsg());
        NumberUtil.isGreaterThanZeroVerify(roomMaxCandidateCountString,
                ResponseCode.ROOM_MAX_CANDIDATE_COUNT_MUST_BE_GREATER_THAN_ZERO.getCode(),
                ResponseCode.ROOM_MAX_CANDIDATE_COUNT_MUST_BE_GREATER_THAN_ZERO.getMsg());
        // 其他校验...
    }

    /**
     * 编排整个考试任务
     * 
     * @param paramMap     编排参数
     * @param taskId       考试任务id
     * @param deleteStatus 是否删除数据重新编排
     * @throws KqException 业务异常
     */
    private void onlineArrangeByTask(Map<String, String> paramMap, String taskId, boolean deleteStatus)
            throws KqException {
        if (deleteStatus) {
            // 重新编排
            // 1.删除已有编排数据
            this.deleteArrangedByOnline(taskId);

            // 2.获取待编排的【考生-科目】数据
            List<CandidateSubjectDTO> candidateSubjectDTOList = iCandidateSubjectService.getCandidateSubjectDTO(taskId);

            // 3.获取【批次-科目】数据
            List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList = iTimeSubjectService
                    .getTimeSubjectCandidateDTO(taskId);

            // 4.将【考生-科目】数据根据【批次-科目】的对应关系放入批次中，组合成【批次-科目-考生】数据
            this.assembleTimeSubjectCandidate(candidateSubjectDTOList, timeSubjectCandidateDTOList);

            // 获取考点数据
            TaskPlace taskPlace = iTaskPlaceService.getOneTaskPlace(taskId);

            // 5.获取考场数据
            List<TaskRoom> taskRoomList = this.getArrangeRoom(timeSubjectCandidateDTOList, paramMap, taskId, taskPlace);

            // 将考生数据放入考场中
            List<TimeRoomCandidate> timeRoomCandidateList = this.assembleRoomCandidate(timeSubjectCandidateDTOList,
                    taskRoomList, taskId, paramMap);

            // 获取场次数据
            List<TimeRoom> timeRoomList = this.assembleTimeRoom(timeRoomCandidateList, taskId, paramMap, taskPlace);

            // 保存编排数据
            this.saveArrangedData(taskRoomList, timeRoomCandidateList, timeRoomList);
        } else {
            // 继续编排
            // 1.获得准考证生成规则

            // 2.获得未编排的考生数据

            // 3.获得未编排的场次数据

            // 4.获得批次数据

        }
    }

    /**
     * 线上机考删除已有编排数据
     * 
     * @param taskId 考试任务id
     */
    private void deleteArrangedByOnline(String taskId) {
        // 删除编排结果表
        this.deleteAllArranged(taskId);
        // 如果是虚拟考场，则删除
        String roomType = iTimeRoomParamService.getTaskTimeRoomParamValueByCode(taskId, StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode());
        if (StringUtils.isBlank(roomType) || "0".equals(roomType)) {
            // 删除考场表
            iTaskRoomService.deleteAllByTaskId(taskId);
            // 删除批次考场表
            iTimeRoomService.deleteAllByTaskId(taskId);
        }
    }

    /**
     * 将【考生-科目】数据根据【批次-科目】的对应关系放入批次中，组合成【批次-科目-考生】数据
     * 
     * @param candidateSubjectDTOList     【考生-科目】数据
     * @param timeSubjectCandidateDTOList 【批次-科目】数据
     */
    private void assembleTimeSubjectCandidate(List<CandidateSubjectDTO> candidateSubjectDTOList,
            List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList) {
        // 分配科目下的考生数
        this.distributionSubjectCandidateCount(candidateSubjectDTOList, timeSubjectCandidateDTOList);

        // 将考生放入批次科目中
        loopCandidate: for (CandidateSubjectDTO candidateSubject : candidateSubjectDTOList) {
            for (TimeSubjectCandidateDTO time : timeSubjectCandidateDTOList) {
                String candidateId = candidateSubject.getId();
                if (time.getSubjectCandidateDTOList().stream()
                        .anyMatch(subjectCandidate -> subjectCandidate.getCandidateList().stream()
                                .anyMatch(candidate -> candidate.getId().equals(candidateId)))) {
                    // 考生已存在当前批次下，继续遍历下一个批次
                    continue;
                }
                for (SubjectCandidateDTO subject : time.getSubjectCandidateDTOList()) {
                    if (subject.getSubjectId().equals(candidateSubject.getSubjectId())
                            && subject.getCandidateList().size() < subject.getCandidateCount()) {
                        // 考生报考的科目与批次下的科目相同，且考生不在批次下，且批次下的考生数小于批次分配考生数，即可将考生分配到批次科目下
                        subject.getCandidateList().add(candidateSubject);
                        // 继续分配下一位考生
                        continue loopCandidate;
                    }
                }
            }
        }
    }

    /**
     * 分配科目下的考生数
     * 
     * @param candidateSubjectDTOList     【考生-科目】数据
     * @param timeSubjectCandidateDTOList 【批次-科目】数据
     */
    private void distributionSubjectCandidateCount(List<CandidateSubjectDTO> candidateSubjectDTOList,
            List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList) {

        // 1.将考生按照科目分组，String是科目id，List<CandidateSubjectDTO>是考生科目数据
        Map<String, List<CandidateSubjectDTO>> subjectCandidateMap = candidateSubjectDTOList.stream()
                .collect(Collectors.groupingBy(CandidateSubjectDTO::getSubjectId));

        // 2.统计每个科目在批次-科目中出现的总次数
        List<SubjectCandidateDTO> subjectCandidateDTOList = new ArrayList<>();
        timeSubjectCandidateDTOList.forEach(timeSubjectCandidateDTO -> subjectCandidateDTOList
                .addAll(timeSubjectCandidateDTO.getSubjectCandidateDTOList()));

        // String是科目id，Long是科目在所有批次中出现的次数
        Map<String, Long> subjectTimeCount = subjectCandidateDTOList.stream()
                .collect(Collectors.groupingBy(SubjectCandidateDTO::getSubjectId, Collectors.counting()));

        // 获取科目与考生分组份数的映射关系，String是科目id，int[]是考生组
        Map<String, int[]> subjectCandidatePartMap = new HashMap<>();
        subjectCandidateMap.forEach((subjectId, candidateList) -> subjectTimeCount.forEach((id, count) -> {
            if (subjectId.equals(id)) {
                // 份数
                int partCount = count.intValue();
                // 科目下考生数
                int subjectCandidateCount = candidateList.size();
                // 使用整数均分算法获取科目考生分组
                int[] candidateParts = MathUtil.averageNumber(subjectCandidateCount, partCount);
                subjectCandidatePartMap.put(subjectId, candidateParts);
            }
        }));

        // 设置科目下的考生数
        subjectCandidatePartMap.forEach((subjectId, candidateParts) -> {
            loop: for (int part : candidateParts) {
                for (TimeSubjectCandidateDTO timeSubjectCandidateDTO : timeSubjectCandidateDTOList) {
                    for (SubjectCandidateDTO subjectCandidateDTO : timeSubjectCandidateDTO
                            .getSubjectCandidateDTOList()) {
                        if (subjectCandidateDTO.getSubjectId().equals(subjectId)
                                && subjectCandidateDTO.getCandidateCount() == 0) {
                            subjectCandidateDTO.setCandidateCount(part);
                            continue loop;
                        }
                    }
                }
            }
        });
    }

    /**
     * 获取待编排的考场
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目-考生】数据
     * @param paramMap                    编排参数
     * @param taskId                      考试任务id
     * @return 待编排考场数据
     */
    private List<TaskRoom> getArrangeRoom(List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList,
            Map<String, String> paramMap, String taskId, TaskPlace taskPlace) {
        // 获取最大考生人数的批次的考生人数
        Map<String, Integer> timeCandidateCount = new HashMap<>();
        timeSubjectCandidateDTOList.forEach(timeSubjectCandidateDTO -> {
            Integer candidateCount = timeSubjectCandidateDTO.getSubjectCandidateDTOList()
                    .stream()
                    .map(subjectCandidateDTO -> subjectCandidateDTO.getCandidateList().size())
                    .mapToInt(x -> x)
                    .sum();
            timeCandidateCount.put(timeSubjectCandidateDTO.getId(), candidateCount);
        });
        int maxCandidateCount = timeCandidateCount.entrySet()
                .stream()
                .max(Map.Entry.comparingByValue())
                .orElseThrow(() -> new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                        ResponseCode.ARRANGE_DATA_EXCEPTION.getMsg()))
                .getValue();
        // 获取考场最大人数
        int roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));
        // 根据考场最大人数和批次最大考生数计算总考场数
        int roomCount = maxCandidateCount / roomMaxCandidateCount;
        if (maxCandidateCount % roomMaxCandidateCount > 0) {
            roomCount++;
        }
        // 使用整数均分递归算法，计算批次下各考场的实际考生数
        List<Integer> roomCandidateParts = new ArrayList<>();
        MathUtil.averageNumberByRecursive(maxCandidateCount, roomCount, roomMaxCandidateCount, roomCandidateParts);
        roomCount = roomCandidateParts.size();
        // 根据考场总数生成考场数据
        List<TaskRoom> taskRoomList = new ArrayList<>();
        for (int i = 0; i < roomCount; i++) {
            TaskRoom taskRoom = new TaskRoom();
            int code = i + 1;
            taskRoom.setTaskId(taskId)
                    .setPlaceId(taskPlace.getId())
                    .setCode(code)
                    .setName("虚拟考场" + code)
                    .setSeatNum(roomMaxCandidateCount)
                    .setReserveSeatNum(0)
                    .setAnswerType(1)
                    .setReserve(false)
                    .setRegistered(false)
                    .setClosure(false)
                    .setId(UUIDUtils.newSortUUID());
            taskRoomList.add(taskRoom);
        }
        return taskRoomList;
    }

    /**
     * 将考生数据放入考场中
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目-考生】数据
     * @param taskRoomList                待编排考场数据
     * @param taskId                      考试任务id
     * @param paramMap                    编排参数
     * @return 编排结果（缺少场次id、座位号、准考证号）
     */
    private List<TimeRoomCandidate> assembleRoomCandidate(List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList,
            List<TaskRoom> taskRoomList, String taskId,
            Map<String, String> paramMap) {
        List<TimeRoomCandidate> timeRoomCandidateList;
        switch (paramMap.get(StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode())) {
            // 完全随机编排
            case "1":
                timeRoomCandidateList = this.arrangeByRandom(timeSubjectCandidateDTOList, taskRoomList, taskId,
                        paramMap);
                break;
            // 按考区编排
            case "0":
                timeRoomCandidateList = this.arrangeByExamArea(timeSubjectCandidateDTOList, paramMap);
                break;
            default:
                throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                        ResponseCode.ARRANGE_DATA_EXCEPTION.getMsg());
        }
        return timeRoomCandidateList;
    }

    /**
     * 获取场次数据
     * 
     * @param timeRoomCandidateList 编排结果数据
     * @param taskId                考试任务id
     * @param paramMap              编排参数
     * @param taskPlace             考点数据
     * @return 场次数据
     */
    private List<TimeRoom> assembleTimeRoom(List<TimeRoomCandidate> timeRoomCandidateList, String taskId,
            Map<String, String> paramMap, TaskPlace taskPlace) {
        // 将编排结果分组，String是"批次id,考场id"组合字符串，List<TimeRoomCandidate>是考生编排数据
        Map<String, List<TimeRoomCandidate>> timeRoomCandidateMap = timeRoomCandidateList.stream()
                .collect(Collectors.groupingBy(
                        timeRoomCandidate -> timeRoomCandidate.getTimeId() + "," + timeRoomCandidate.getRoomId()));
        // 获取最大考场最大人数
        Integer roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));
        // 生成场次数据
        List<TimeRoom> timeRoomList = new ArrayList<>();
        // 生成座位号（编排结果id，座位号）
        Map<String, String> idSeatNumberMap = new HashMap<>();
        timeRoomCandidateMap.forEach((timeRoomId, candidateList) -> {
            // 生成场次数据
            TimeRoom timeRoom = new TimeRoom();
            String[] timeRoomIdArray = timeRoomId.split(",");
            timeRoom.setTaskId(taskId)
                    .setTimeId(timeRoomIdArray[0])
                    .setRoomId(timeRoomIdArray[timeRoomIdArray.length - 1])
                    .setPlanCount(roomMaxCandidateCount)
                    .setMaxCount(roomMaxCandidateCount)
                    .setActualCount(candidateList.size())
                    .setId(UUIDUtils.newSortUUID());
            timeRoomList.add(timeRoom);
            // 生成座位号（编排结果id，座位号）
            for (int i = 0; i < candidateList.size(); i++) {
                String seatNumber = StringUtil.stringFormatZero(i + 1, 3);
                idSeatNumberMap.put(candidateList.get(i).getId(), seatNumber);
            }
        });
        // 生成准考证号
        this.generateAdmissionNumber(taskId, taskPlace, paramMap, timeRoomCandidateList);
        // 设置编排结果中的场次id、座位号
        timeRoomCandidateList.forEach(timeRoomCandidate -> {
            timeRoomList.forEach(timeRoom -> {
                // 设置场次id
                if (timeRoomCandidate.getTimeId().equals(timeRoom.getTimeId()) &&
                        timeRoomCandidate.getRoomId().equals(timeRoom.getRoomId())) {
                    timeRoomCandidate.setTimeRoomId(timeRoom.getId());
                }
            });
            // 设置座位号
            idSeatNumberMap.forEach((id, seatNumber) -> {
                if (timeRoomCandidate.getId().equals(id)) {
                    timeRoomCandidate.setSeatNum(seatNumber);
                }
            });
        });
        return timeRoomList;
    }

    /**
     * 保存编排数据
     * 
     * @param taskRoomList          考场数据
     * @param timeRoomCandidateList 编排数据
     * @param timeRoomList          场次数据
     */
    private void saveArrangedData(List<TaskRoom> taskRoomList, List<TimeRoomCandidate> timeRoomCandidateList,
            List<TimeRoom> timeRoomList) {
        // 保存考场数据
        if (!iTaskRoomService.saveBatch(taskRoomList)) {
            throw new KqException(ResponseCode.ROOM_ADD_EXCEPTION.getCode(),
                    ResponseCode.ROOM_ADD_EXCEPTION.getMsg());
        }
        // 保存场次数据
        if (!iTimeRoomService.saveBatch(timeRoomList)) {
            throw new KqException(ResponseCode.FAILED_TO_SAVE_TIME_ROOM.getCode(),
                    ResponseCode.FAILED_TO_SAVE_TIME_ROOM.getMsg());
        }
        // 保存编排结果数据
        if (!saveBatch(timeRoomCandidateList)) {
            throw new KqException(ResponseCode.FAILED_TO_SAVE_ARRANGED_DATA.getCode(),
                    ResponseCode.FAILED_TO_SAVE_ARRANGED_DATA.getMsg());
        }
    }

    /**
     * 生成准考证号
     * 
     * @param taskId                考试任务id
     * @param taskPlace             考点数据
     * @param paramMap              编排参数
     * @param timeRoomCandidateList 编排数据
     */
    private void generateAdmissionNumber(String taskId, TaskPlace taskPlace, Map<String, String> paramMap,
            List<TimeRoomCandidate> timeRoomCandidateList) {
        TaskPlaceVO taskPlaceVO = new TaskPlaceVO();
        taskPlaceVO.setProvince(taskPlace.getProvince())
                .setCity(taskPlace.getCity())
                .setDistrict(taskPlace.getDistrict());
        // 获取准考证生成方式
        // 目前暂时只支持同个考生不同批次，准考证号相同，预计后续会支持同个考生不同批次，准考证号不同
        // String admissionNumberMode = paramMap.get("admissionNumberMode");
        String admissionNumberMode = "1";
        switch (admissionNumberMode) {
            // 同个考生不同批次，准考证号相同
            case "1":
                this.generateAdmissionNumberBySame(taskId, paramMap, timeRoomCandidateList, taskPlaceVO);
                break;
            // 同个考生不同批次，准考证号不同
            case "2":
                this.generateAdmissionNumberByDifferent();
                break;
            default:
                throw new KqException(ResponseCode.ARRANGE_DATA_EXCEPTION.getCode(),
                        ResponseCode.ARRANGE_DATA_EXCEPTION.getMsg());
        }
    }

    /**
     * 生成准考证号，并入库
     * 
     * @param taskId
     * @param paramMap
     * @param timeRoomCandidateList
     */
    private void generateAdmissionNumberAndInsert(String taskId, Map<String, String> paramMap,
            List<TimeRoomCandidate> timeRoomCandidateList,
            TaskPlaceVO taskPlaceVO) {
        // 获取准考证号生成方式
        Integer admissionType = new Integer(paramMap.get("admissionType"));
        if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
            // 使用自定义准考证号编排
            this.getAdmissionNumberByCustom(taskId, timeRoomCandidateList);
        } else {
            // 使用准考证生成规则生成准考证
            Map<String, String> candidateIdAdmissionNumberMap = new HashMap<>();
            timeRoomCandidateList.forEach(timeRoomCandidate -> {
                String candidateId = timeRoomCandidate.getCandidateId();
                String admissionNumber = candidateIdAdmissionNumberMap.get(candidateId);
                if (StringUtils.isBlank(admissionNumber)) {
                    admissionNumber = this.getAdmissionNumber(taskId, paramMap, taskPlaceVO);
                    candidateIdAdmissionNumberMap.put(candidateId, admissionNumber);
                }
                timeRoomCandidate.setAdmissionNum(admissionNumber);
            });
        }
        // 入库
        this.saveBatch(timeRoomCandidateList);
    }

    private void getAdmissionNumberByCustom(String taskId, List<TimeRoomCandidate> timeRoomCandidateList) {
        // 获取已有准考证号的考生数据
        List<CustomArrange> customArrangeList = iCustomArrangeService.listByTaskId(taskId);
        // 为所有待编排考生设置准考证号
        timeRoomCandidateList.forEach(timeRoomCandidate -> {
            // 获取待编排考生的准考证号，并校验是否所有待编排考生都有准考证号
            String admissionNumber = customArrangeList.stream()
                    .filter(candidateAdmissionNumber -> timeRoomCandidate.getCandidateId()
                            .equals(candidateAdmissionNumber.getCandidateId()))
                    .findFirst()
                    .map(CustomArrange::getAdmissionNumber)
                    .orElseThrow(
                            () -> new KqException(ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getCode(),
                                    ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getMsg()));
            timeRoomCandidate.setAdmissionNum(admissionNumber);
        });
    }

    /**
     * 生成准考证号，同个考生不同批次，准考证号相同
     * 
     * @param taskId                考试任务id
     * @param paramMap              编排参数
     * @param timeRoomCandidateList 编排考生数据
     * @param taskPlaceVO           考点信息vo
     */
    private void generateAdmissionNumberBySame(String taskId, Map<String, String> paramMap,
            List<TimeRoomCandidate> timeRoomCandidateList, TaskPlaceVO taskPlaceVO) {
        // 同个考生不同批次，准考证号相同
        // 获取所有考生的id
        List<String> candidateIdList = timeRoomCandidateList.stream()
                .map(TimeRoomCandidate::getCandidateId)
                .collect(Collectors.toList())
                .stream()
                .distinct()
                .collect(Collectors.toList());
        // 为所有考生生成准考证号(考生id,准考证号)
        Map<String, String> candidateIdAdmissionNumberMap = new HashMap<>();
        candidateIdList.forEach(candidateId -> {
            String admissionNumber = this.getAdmissionNumber(taskId, paramMap, taskPlaceVO);
            candidateIdAdmissionNumberMap.put(candidateId, admissionNumber);
        });
        // 设置准考证号
        timeRoomCandidateList
                .forEach(timeRoomCandidate -> candidateIdAdmissionNumberMap.forEach((candidateId, admissionNumber) -> {
                    if (timeRoomCandidate.getCandidateId().equals(candidateId)) {
                        timeRoomCandidate.setAdmissionNum(admissionNumber);
                    }
                }));
    }

    /**
     * 生成准考证号，同个考生不同批次，准考证号不同
     */
    private void generateAdmissionNumberByDifferent() {
        // 生成准考证号，同个考生不同批次，准考证号不同
    }

    /**
     * 完全随机编排
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目-考生】数据
     * @param taskRoomList                待编排考场数据
     * @param taskId                      考试任务id
     * @param paramMap                    编排参数
     * @return 编排结果(缺少场次id、座位号、准考证号)
     */
    private List<TimeRoomCandidate> arrangeByRandom(List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList,
            List<TaskRoom> taskRoomList, String taskId, Map<String, String> paramMap) {
        // 编排结果数据
        List<TimeRoomCandidate> timeRoomCandidateList = new ArrayList<>();

        // 从编排参数中获取考场最大考生数
        int roomMaxCandidateCount = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ROOM_MAX_CANDIDATE_COUNT.getCode()));

        // 多科目混编
        timeSubjectCandidateDTOList.forEach(timeSubjectCandidateDTO -> {
            // 遍历批次
            // 计算批次考生数
            int timeCandidateCount = timeSubjectCandidateDTO.getSubjectCandidateDTOList()
                    .stream()
                    .map(subjectCandidateDTO -> subjectCandidateDTO.getCandidateList().size())
                    .mapToInt(x -> x)
                    .sum();
            // 粗略计算批次所需考场数
            int roomCount = timeCandidateCount / roomMaxCandidateCount;
            if (timeCandidateCount % roomMaxCandidateCount > 0) {
                roomCount++;
            }
            // 使用整数均分递归算法，计算批次下各考场的实际考生数
            List<Integer> roomCandidateParts = new ArrayList<>();
            MathUtil.averageNumberByRecursive(timeCandidateCount, roomCount, roomMaxCandidateCount, roomCandidateParts);
            // 创建批次-考场-考生数据结构
            List<TimeRoomCandidateDTO> timeRoomCandidateDTOList = new ArrayList<>();
            for (int i = 0; i < roomCandidateParts.size(); i++) {
                TimeRoomCandidateDTO timeRoomCandidateDTO = new TimeRoomCandidateDTO();
                String timeId = timeSubjectCandidateDTO.getId();
                String roomId = taskRoomList.get(i).getId();
                timeRoomCandidateDTO.setTimeId(timeId)
                        .setRoomId(roomId)
                        .setCandidateCount(roomCandidateParts.get(i));
                timeRoomCandidateDTOList.add(timeRoomCandidateDTO);
            }
            // 当前批次下的考生数据放入批次-考场-考生数据结构中
            // 当前批次下遍历科目
            for (SubjectCandidateDTO subjectCandidateDTO : timeSubjectCandidateDTO.getSubjectCandidateDTOList()) {
                // 科目下遍历考生
                loop: for (CandidateSubjectDTO candidate : subjectCandidateDTO.getCandidateList()) {
                    // 考生数据放入批次-考场-考生数据结构中
                    for (TimeRoomCandidateDTO timeRoomCandidateDTO : timeRoomCandidateDTOList) {
                        // 当前场次下的考生数小于计划安排考生数时可将考生安排到当前场次下
                        if (timeRoomCandidateDTO.getCandidateList().size() < timeRoomCandidateDTO.getCandidateCount()) {
                            timeRoomCandidateDTO.getCandidateList().add(candidate);
                            // 继续遍历下一位考生
                            continue loop;
                        }
                    }
                }
            }
            // 生成编排结果数据
            timeRoomCandidateDTOList
                    .forEach(timeRoomCandidateDTO -> timeRoomCandidateDTO.getCandidateList().forEach(candidate -> {
                        TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
                        timeRoomCandidate.setCandidateId(candidate.getId())
                                .setTaskId(taskId)
                                .setTimeId(timeRoomCandidateDTO.getTimeId())
                                .setRoomId(timeRoomCandidateDTO.getRoomId())
                                .setSubjectId(candidate.getSubjectId())
                                .setCandidateSubjectId(candidate.getCandidateSubjectId())
                                .setId(UUIDUtils.newSortUUID());
                        timeRoomCandidateList.add(timeRoomCandidate);
                    }));
        });
        return timeRoomCandidateList;
        // 多科目分开考场编排
    }

    /**
     * 按照考区编排
     * 
     * @param timeSubjectCandidateDTOList 【批次-科目-考生】数据
     * @param paramMap                    编排参数
     */
    private List<TimeRoomCandidate> arrangeByExamArea(List<TimeSubjectCandidateDTO> timeSubjectCandidateDTOList,
            Map<String, String> paramMap) {
        // 按照考区编排
        return Collections.emptyList();
    }

    /**
     * 编排考试任务下指定批次
     * 
     * @param paramMap     编排参数
     * @param taskId       考试任务id
     * @param deleteStatus 是否删除数据重新编排
     * @throws KqException 业务异常
     */
    private void onlineArrangeByTime(Map<String, String> paramMap, String taskId, boolean deleteStatus)
            throws KqException {
        // 编排考试任务下指定批次
    }

    /**
     * 线下机考编排
     * 
     * @param paramMap     编排参数
     * @param taskId       考试任务id
     * @param deleteStatus 是否删除数据重新编排
     * @throws KqException 业务异常
     */
    public void offlineArrange(Map<String, String> paramMap, String taskId, boolean deleteStatus) throws KqException {
        // 否删除数据重新编排，如果是编排页面，则删除，如果是在考生未编排的列表编排，则不用删除
        if (deleteStatus) {
            // 重新编排的情况下，删除所有编排数据
            timeRoomCandidateMapper.deleteArrangeCandidateByTaskId(taskId);
        }
        // 4.获取未编排的考生数据，一个考生多个科目共一条数据
        Map<String, CandidateArrangePreVO> candidateMap = iCandidateSubjectService.getCandidateNeedArrange(taskId);
        // 5.校验，如果按区编排，则考生必须有区域信息。
        if (String.valueOf(ArrangeParamCodeEnum.ARRANGE_TYPE_AREA.getCode())
                .equals(paramMap.get(StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode()))) {
            iCandidateService.checkCandidateAreaThrow(taskId);
        }
        // 6.获取科次信息--批次科目
        Map<String, String> timeSubjectMap = timeSubjectService.getTimeSubjectMap(taskId);
        if (timeSubjectMap == null || timeSubjectMap.isEmpty()) {
            throw new KqException(ResponseCode.ARRANGE_CANDIDATE_NOT_TIME_ROOM.getCode(),
                    ResponseCode.ARRANGE_CANDIDATE_NOT_TIME_ROOM.getMsg());
        }
        // 7.获取考点数据：原则考生编排到同一个考点，不要编排到多个考点去
        List<TaskPlaceVO> taskPlaceVOList = iTimeRoomService.selectNoArrangePlaceIds(taskId);
        if (taskPlaceVOList == null || taskPlaceVOList.isEmpty()) {
            throw new KqException(ResponseCode.ARRANGE_CANDIDATE_NOT_ROOM.getCode(),
                    ResponseCode.ARRANGE_CANDIDATE_NOT_ROOM.getMsg());
        }
        // 8.考区查询
        List<String> examAreaList = examAreaMapper.selectAreaAndStudentAreaCode(taskId);
        // 9.获取机构类型：OBT，GCE，编排的区别是：OBT区域少了一个级别。
        // String code = TaskConfigCodeEnum.EXAM_SYSTEM_TYPE.getCode();
        // String examType = iTaskConfigService.getTaskConfigValue(taskId, code);
        // paramMap.put(code, examType);

        // 获取考生准考证号数据
        List<CustomArrange> customArrangeList = null;
        Integer admissionType = new Integer(paramMap.get("admissionType"));
        if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
            // 使用自定义准考证号编排
            customArrangeList = iCustomArrangeService.listByTaskId(taskId);
        }

        String cacheKey = ARRANGE_CANDIDATE_KEY.getCacheEnumKey(taskId);
        // 该任务是否正在执行编排(分布式锁)，避免多用户操作同一任务
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(cacheKey, true,
                ARRANGE_CANDIDATE_KEY.getExpireSeconds(), TimeUnit.SECONDS);
        if (lockFlag) {
            try {
                // 9.开始编排数据
                arrangeCandidate(taskId, taskPlaceVOList, candidateMap, paramMap, timeSubjectMap, examAreaList,
                        customArrangeList);
            } finally {
                redisTemplate.delete(cacheKey);
            }
        } else {
            log.info("# 任务正在编排中，重复操作：{}", taskId);
        }
    }

    /**
     * 编排--全国随机编排
     * 
     * @param taskId
     * @param taskPlaceVOList
     * @param candidateMap
     * @param paramMap
     * @param timeSubjectMap
     */
    private void arrangeCandidate(String taskId, List<TaskPlaceVO> taskPlaceVOList,
            Map<String, CandidateArrangePreVO> candidateMap,
            Map<String, String> paramMap, Map<String, String> timeSubjectMap,
            List<String> examAreaList, List<CustomArrange> customArrangeList) {
        // 编排成功的考生，需要入库
        List<TimeRoomCandidate> timeRoomCandidateAllList = new ArrayList<>();

        // 6 考点开始编排
        taskPlaceVOList.stream().forEach(taskPlaceVO -> {

            // 6.1 考点下所有的未编排的场次数据
            if (taskPlaceVO.getTimeRoomVOList() == null || taskPlaceVO.getTimeRoomVOList().isEmpty()) {
                taskPlaceVO.setTimeRoomVOList(iTimeRoomService.selectTimeRoomByPlaceId(taskId, taskPlaceVO.getId()));
            }

            // 场次数据需要更新入库
            Map<String, Integer> timeRoomActualCountMap = Maps.newHashMap();

            // 6.2 遍历考生
            candidateMap.forEach((candidateId, candidateArrangePreVO) -> {

                // 6.2.0、编排方式，如果是混编，则不需要匹配考区，如果是按区编排，则一次匹配县区，市，省，考区等循序编排
                if (!getArrangeStatus(paramMap, candidateArrangePreVO, taskPlaceVO, examAreaList)) {
                    return;
                }

                // 考生临时编排数据
                List<TimeRoomCandidate> tempTimeRoomCandidateList = new ArrayList<>();
                // 批次临时编排数据
                Map<String, String> tempTimeMap = Maps.newHashMap();
                // 遍历科目-编排到场次去
                List<CandidateArrangeSubjectPreVO> candidateArrangeSubjectPreVOList = candidateArrangePreVO
                        .getCandidateArrangeSubjectPreVOList();
                // 6.2.1 遍历考生下，所有的科目
                candidateArrangeSubjectPreVOList.stream().forEach(candidateArrangeSubjectPreVO -> {
                    // 6.2.1.1 遍历该考点下所有的场次
                    taskPlaceVO.getTimeRoomVOList().stream().anyMatch(timeRoomVO -> {
                        // 1、如果这个批次已编排过，则跳过本次循环，寻找下个场次
                        if (tempTimeMap.containsKey(timeRoomVO.getTimeId())) {
                            return false;
                        }

                        // 2、如果实际编排数和考场的最大编排书，意味着已编满，无需再编排，则跳过本次循环，寻找下个场次
                        if (timeRoomVO.getActualCount().intValue() >= timeRoomVO.getMaxCount().intValue()) {
                            return false;
                        }

                        // 3、如果该考生科目，在当前的批次下找不到，则跳过本次循环，寻找下个场次
                        String timeSubject = StringUtils.join(timeRoomVO.getTimeId(), "_",
                                candidateArrangeSubjectPreVO.getSubjectId());
                        if (!timeSubjectMap.containsKey(timeSubject)) {
                            return false;
                        }

                        // 4、如果考生的考试类型(电子作答、纸笔作答)和考场的考试类型不一致，则跳过本次循环，寻找下个场次
                        if (timeRoomVO.getAnswerType().intValue() != candidateArrangeSubjectPreVO.getAnswerType()
                                .intValue()) {
                            return false;
                        }

                        // 5、开始编排，将编排的批次添加到临时批次编排中，下个科目再次编排则不能编排到本批次
                        tempTimeMap.put(timeRoomVO.getTimeId(), "");

                        // 6、开始编排，组装考生临时编排数据，不包含座位号和准考证号，确认要编排时，再添加。
                        TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
                        timeRoomCandidate.setCandidateId(candidateId)
                                .setTaskId(taskId)
                                .setRoomId(timeRoomVO.getRoomId())
                                .setTimeRoomId(timeRoomVO.getId())
                                .setTimeId(timeRoomVO.getTimeId())
                                .setSubjectId(candidateArrangeSubjectPreVO.getSubjectId())
                                .setCandidateSubjectId(candidateArrangeSubjectPreVO.getCandidateSubjectId())
                                .setId(UUIDUtils.newSortUUID());

                        tempTimeRoomCandidateList.add(timeRoomCandidate);
                        return true;
                    });
                });

                // 6.2.2 单个考生成功编排，含多个科目
                if (candidateArrangeSubjectPreVOList.size() == tempTimeRoomCandidateList.size()) {
                    // 6.2.2.1 遍历临时编排数据
                    tempTimeRoomCandidateList.stream().forEach(timeRoomCandidate -> {
                        // 准考证号
                        Integer admissionType = new Integer(paramMap.get("admissionType"));
                        if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
                            // 使用自定义准考证号编排
                            String admissionNumber = customArrangeList.stream()
                                    .filter(candidateAdmissionNumber -> timeRoomCandidate.getCandidateId()
                                            .equals(candidateAdmissionNumber.getCandidateId()))
                                    .findFirst()
                                    .map(CustomArrange::getAdmissionNumber)
                                    .orElseThrow(() -> new KqException(
                                            ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getCode(),
                                            ResponseCode.ARRANGE_CANDIDATE_ADMISSION_NUMBER_NOT_FOUND.getMsg()));
                            timeRoomCandidate.setAdmissionNum(admissionNumber);
                        } else {
                            // 使用准考证号生成规则生成准考证号
                            String admissionNum = getAdmissionNumber(taskId, paramMap, taskPlaceVO);
                            timeRoomCandidate.setAdmissionNum(admissionNum);
                        }
                        // 场次编排人数更新
                        taskPlaceVO.getTimeRoomVOList().stream().anyMatch(timeRoomVO -> {
                            if (timeRoomCandidate.getTimeRoomId().equals(timeRoomVO.getId())) {

                                // 要在actualCount增加之前调用
                                String seatNum = getSeatNum(timeRoomVO);
                                timeRoomCandidate.setSeatNum(seatNum);

                                // actualCount增加
                                timeRoomVO.setActualCount(timeRoomVO.getActualCount() + 1);

                                timeRoomActualCountMap.put(timeRoomVO.getId(), timeRoomVO.getActualCount());

                                return true;
                            }
                            return false;
                        });
                    });

                    // 6.2.2.2 将考生临时编排数据放入到编排数据中，等待入库
                    timeRoomCandidateAllList.addAll(tempTimeRoomCandidateList);
                    // 6.2.2.3 将成功编排的考生从考生编排中置空，等待下个考点编排时删除
                    candidateMap.put(candidateId, null);
                }

                // 6.2.3 考生编排数据入库，10000条入库一次
                int maxData = 10000;
                if (timeRoomCandidateAllList.size() > maxData) {
                    boolean flag = timeRoomCandidateService.saveBatch(timeRoomCandidateAllList);
                    if (flag) {
                        timeRoomCandidateAllList.clear();
                    }
                }

                // 一个考生循环结束==================

            });

            // 6.3 更新编排的考生数据
            boolean flag = timeRoomCandidateService.saveBatch(timeRoomCandidateAllList);
            if (flag) {
                timeRoomCandidateAllList.clear();
            }

            // 6.4 更新场次的实际编排数
            if (timeRoomActualCountMap != null && !timeRoomActualCountMap.isEmpty()) {
                // 需要更新的场次信息
                List<TimeRoom> timeRoomUpdateList = new ArrayList<TimeRoom>();
                timeRoomActualCountMap.forEach((k, v) -> {
                    TimeRoom timeRoom = new TimeRoom();
                    timeRoom.setId(k);
                    timeRoom.setActualCount(v);
                    timeRoomUpdateList.add(timeRoom);
                });

                // 批量更新场次数据
                iTimeRoomService.updateBatchById(timeRoomUpdateList);

                // 6.5 将编排的考生从未编排的考生数据中删除
                candidateMap.values().removeIf(v -> v == null);
            }

            // 所有考生循环结束==================
        });

        // 7 再次编排--如果是按区域编排，则扩大编排范围：区域，市，省，考区。
        forEachArrangeCandidate(taskId, taskPlaceVOList, candidateMap, paramMap, timeSubjectMap, examAreaList,
                customArrangeList);
    }

    /**
     * 是否循环编排考生
     */
    private void forEachArrangeCandidate(String taskId, List<TaskPlaceVO> taskPlaceVOList,
            Map<String, CandidateArrangePreVO> candidateMap, Map<String, String> paramMap,
            Map<String, String> timeSubjectMap, List<String> examAreaList,
            List<CustomArrange> customArrangeList) {

        // 如果是混编，不需要循环执行
        if (String.valueOf(ArrangeParamCodeEnum.ARRANGE_TYPE_MIX.getCode()).equals(
                paramMap.get(StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode()))) {
            return;
        }

        int auto = Integer.parseInt(String.valueOf(paramMap.get("areaArrangeAuto")));
        if (auto < ArrangeParamCodeEnum.AREA_ARRANGE_LEVEL_EXAM_AREA.getCode().intValue()) {
            paramMap.put("areaArrangeAuto", String.valueOf(++auto));
            this.arrangeCandidate(taskId, taskPlaceVOList, candidateMap, paramMap, timeSubjectMap, examAreaList,
                    customArrangeList);
        }
    }

    /**
     * 区域查找
     * 
     * @param paramMap
     * @param candidateArrangePreVO
     * @param taskPlaceVO
     * @return
     */
    private boolean getArrangeStatus(Map<String, String> paramMap, CandidateArrangePreVO candidateArrangePreVO,
            TaskPlaceVO taskPlaceVO, List<String> examAreaList) {

        // 1、全国混编
        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode())
                .equals(ArrangeParamCodeEnum.ARRANGE_TYPE_MIX.getCode().toString())) {
            return true;
        }

        // 2、编排方式，如果是混编，则不需要匹配考区，如果是按区编排，则一次匹配县区，市，省，考区等循序编排
        String areaArrangeAuto = paramMap.get("areaArrangeAuto");

        // 2.1 按区域编排
        if (areaArrangeAuto.equals(ArrangeParamCodeEnum.AREA_ARRANGE_LEVEL_DISTRICT.getCode().toString())) {
            if (!candidateArrangePreVO.getDistrict().equals(taskPlaceVO.getDistrict())) {
                return false;
            }
        }

        // 2.2 按市编排
        if (areaArrangeAuto.equals(ArrangeParamCodeEnum.AREA_ARRANGE_LEVEL_CITY.getCode().toString())) {
            if (!candidateArrangePreVO.getCity().equals(taskPlaceVO.getCity())) {
                return false;
            }
        }

        // 2.3 按省编排
        if (areaArrangeAuto.equals(ArrangeParamCodeEnum.AREA_ARRANGE_LEVEL_PROVINCE.getCode().toString())) {
            if (!candidateArrangePreVO.getProvince().equals(taskPlaceVO.getProvince())) {
                return false;
            }
        }

        // 2.4 按考区编排
        if (areaArrangeAuto.equals(ArrangeParamCodeEnum.AREA_ARRANGE_LEVEL_EXAM_AREA.getCode().toString())) {
            String candidateExamArea = StringUtils.join(taskPlaceVO.getProvince(), "_",
                    candidateArrangePreVO.getProvince());

            if (!examAreaList.contains(candidateExamArea)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 获取座位号
     * 
     * @param timeRoomVO
     * @return
     */
    private String getSeatNum(TimeRoomVO timeRoomVO) {
        // 1.组装座位号
        int seatNum = timeRoomVO.getActualCount() + 1;
        if (seatNum > timeRoomVO.getMaxCount()) {
            // 座位号大于最大编排数，说明中间后空缺的座位号，则去查库
            List<String> seatNumList = timeRoomCandidateMapper.selectArrangeCandidateSeatNum(timeRoomVO);
            for (int i = 0; i < seatNumList.size(); i++) {
                int num = Integer.parseInt(seatNumList.get(i));
                int numAuto = i + 1;
                if (numAuto != num) {
                    seatNum = numAuto;
                    break;
                }
            }
        }

        return StringUtil.stringFormatZero(seatNum, 3);
    }

    /**
     * 获取准考证号
     * 
     * @param taskId
     * @param paramMap
     * @param taskPlaceVO
     * @return
     */
    public String getAdmissionNumber(String taskId, Map paramMap, TaskPlaceVO taskPlaceVO) {
        // 获取准考证号生成方式
        int admissionType = Integer.parseInt(String.valueOf(paramMap.get("admissionType")));
        // 组装准考证号
        if (admissionType == ArrangeParamCodeEnum.ADMISSION_TYPE_GROUP.getCode().intValue()) {
            // 准考证方式-组合
            return getAdmissionGroup(taskId, paramMap, taskPlaceVO);
        } else if (admissionType == ArrangeParamCodeEnum.ADMISSION_TYPE_MANUAL.getCode().intValue()) {
            // 准考证方式-手动
            return getAdmissionManual(taskId, paramMap);
        } else if (admissionType == ArrangeParamCodeEnum.ADMISSION_TYPE_RANDOM.getCode().intValue()) {
            // 准考证方式-随机
            return getAdmissionRandom(taskId, paramMap);
        }
        return null;
    }

    /**
     * 获取准考证号-分组
     * 
     * @param taskId
     * @param paramMap
     * @param taskPlaceVO
     * @return
     */
    private String getAdmissionGroup(String taskId, Map paramMap, TaskPlaceVO taskPlaceVO) {

        StringBuilder builder = new StringBuilder();
        String statusOn = "true";
        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_YEAR.getCode()).equals(statusOn)) {
            int groupYearDigit = Integer.parseInt(String.valueOf(paramMap.get("groupYearDigit")));
            builder.append(DateTimeUtil.getFormatYear(groupYearDigit));
        }

        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_PROVINCE.getCode()).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(paramMap.get("groupProvinceDigit")));
            builder.append(StringUtil.stringRight(taskPlaceVO.getProvince(), digit));
        }

        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_CITY.getCode()).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(paramMap.get("groupCityDigit")));
            builder.append(StringUtil.stringRight(taskPlaceVO.getCity(), digit));
        }

        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_DISTRICT.getCode()).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(paramMap.get("groupDistrictDigit")));
            builder.append(StringUtil.stringRight(taskPlaceVO.getDistrict(), digit));
        }

        String key = StringUtils.isBlank(builder.toString()) ? "NoArea" : builder.toString();

        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_RANDOM.getCode()).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(paramMap.get("groupRandomDigit")));
            builder.append(StringUtil.randomNumber(digit));
        }

        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_GROUP_AUTO.getCode()).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(paramMap.get("groupAutoDigit")));

            int maxAuto = 0;
            if (paramMap.get(key) == null) {
                maxAuto = timeRoomCandidateMapper.selectMaxSeaNumByGroup(taskId, digit, taskPlaceVO.getProvince(),
                        taskPlaceVO.getCity(), taskPlaceVO.getDistrict());
            } else {
                maxAuto = Integer.parseInt(String.valueOf(paramMap.get(key)));
            }

            builder.append(StringUtil.stringFormatZero((maxAuto + 1), digit));

            paramMap.put(key, maxAuto + 1);
        }

        return builder.toString();
    }

    /**
     * 获取准考证号-手动
     * 
     * @param taskId
     * @param paramMap
     * @return
     */
    private String getAdmissionManual(String taskId, Map paramMap) {

        // 1.获取自增的最大值
        int digit = Integer.parseInt(String.valueOf(paramMap.get("manualIncreaseDigit")));
        int max = 0;
        if (paramMap.get(StringCodeEnum.ARRANGE_PARAM_MANUAL_AUTO_MAX.getCode()) == null) {
            // 查询数据库
            max = timeRoomCandidateMapper.selectMaxSeaNumByManual(taskId, digit);
        } else {
            max = Integer.parseInt(String.valueOf(paramMap.get("manualAutoMax")));
        }

        // 获取随机数位数
        int randomNumberDigits = Integer
                .parseInt(paramMap.get(StringCodeEnum.ARRANGE_PARAM_RANDOM_NUMBER_DIGIT.getCode()).toString());

        // 2.加一，并格式化补全0
        StringBuilder builder = new StringBuilder();
        // 自定义前缀
        builder.append(paramMap.get("manual"));
        // 随机数
        if (randomNumberDigits > 0) {
            builder.append(RandomUtil.randomNumbers(randomNumberDigits));
        }
        // 自增序号
        builder.append(StringUtil.stringFormatZero((max + 1), digit));

        paramMap.put("manualAutoMax", (max + 1));
        return builder.toString();
    }

    /**
     * 生成准考证号-随机
     * 
     * @param taskId
     * @param paramMap
     * @return
     */
    private String getAdmissionRandom(String taskId, Map paramMap) {

        String statusOn = CodeEnum.PUBLISHED.getCode().toString();
        int digit = Integer.parseInt(String.valueOf(paramMap.get("randomDigit")));

        List<String> list = (List<String>) paramMap.get("admissionNumber_random");
        if (list == null || list.isEmpty()) {
            list = timeRoomCandidateMapper.selectAllSeaNumByRandom(taskId);
        }

        if (list == null || list.isEmpty()) {
            list = new ArrayList<>();
        }

        String admissionNumber = null;
        String randomParam = String.valueOf(paramMap.get("randomParam"));
        while (StringUtils.isBlank(admissionNumber)) {
            admissionNumber = StringUtil.randomStringOrNumberNew(digit, randomParam);
            if (list.contains(admissionNumber)) {
                admissionNumber = null;
            }
        }

        list.add(admissionNumber);
        paramMap.put("admissionNumber_random", list);

        return admissionNumber;
    }

    /**
     * 查询考生编排结果
     * 
     * @return
     */
    @Override
    public Page<TimeRoomCandidateVO> getArrangeCandidate(PageParam<TimeRoomCandidateVO> pageParam,
            ArrangeCandidateQueryParamVO arrangeCandidateQueryParamVO) {

        Page<TimeRoomCandidateVO> page = timeRoomCandidateMapper.selectTimeRoomCandidatePage(pageParam,
                arrangeCandidateQueryParamVO);

        return page;
    }

    /**
     * 查询未编排的考生数据
     * 
     * @param pageParam
     * @return
     */
    @Override
    public Page<CandidateSubjectNoArrangeVO> getNoArrangeCandidatePage(PageParam<CandidateSubjectNoArrangeVO> pageParam,
            ArrangeNoCandidateQueryParamVO arrangeNoCandidateQueryParamVO) {
        Page<CandidateSubjectNoArrangeVO> page = timeRoomCandidateMapper.selectNoArrangeCandidatePage(pageParam,
                arrangeNoCandidateQueryParamVO);
        return page;
    }

    /**
     * 取消编排
     * 
     * @param taskId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeArrangeCandidate(String candidateIds, String taskId) {
        log.info("清除编排数据，任务id：{}，编排ID：{}", taskId, candidateIds);
        // 根据考生id删除数据，如果考生id为空，则删除所有
        if(StrUtil.isBlank(candidateIds)){
            throw new KqException(ResponseCode.ERROR_CANDIDATE_ID_CANNOT_BE_EMPTY.getCode(), "调整考生时,考生id不能为空");
        }

        timeRoomCandidateMapper.deleteArrangeCandidateByCandidateIds(candidateIds, taskId);

        // 如果编排数据全部删除，则虚拟考场模式下，需要清除虚拟考场，场次等数据。
        Integer count = lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .count();

        if (count == 0) {
            Task task = iTaskService.getTaskById(taskId);
            if (task != null && TaskExamTypeEnum.ONLINE.getCode().equals(task.getExamType())) {
                log.info("线上模式清除编排数据，任务id：{}", taskId);
                // 如果是虚拟考场，则删除
                String roomType = iTimeRoomParamService.getTaskTimeRoomParamValueByCode(taskId, StringCodeEnum.ARRANGE_PARAM_ARRANGE_TYPE.getCode());
                if (StringUtils.isBlank(roomType) || "0".equals(roomType)) {
                    iTaskPlaceService.lambdaUpdate().set(BaseEntity::getDelFlag, true).eq(TaskPlace::getTaskId, taskId).eq(BaseEntity::getDelFlag, false).update();
                    // 删除考场表
                    iTaskRoomService.deleteAllByTaskId(taskId);
                }
                iProctorService.resetProctorArrange(taskId);
            }
        }
    }

    private ImmutablePair<List<List<Object>>, List<List<String>>> getArrangeResultByRoom(String taskId) {
        List<List<String>> head = new ArrayList<>();
        List<String> headRow = new ArrayList<>(Arrays.asList("考场名称","所属机构","所属区域","考位数"));
        for (String s : headRow) {
            List<String> head0 = new ArrayList<>();
            head0.add(s);
            head.add(head0);
        }
        List<Time> timeList = iTimeService.lambdaQuery().eq(Time::getTaskId, taskId)
                .eq(BaseEntity::getDelFlag, false).orderByAsc(Time::getCode).list();
        timeList.stream().forEach(time -> {
            List<String> head0 = new ArrayList<>();
            head0.add(time.getName());
            head.add(head0);
        });

        List<List<Object>> data = new ArrayList<>();
        int current = 1;
        ArrangeResultByRoomParam param = new ArrangeResultByRoomParam();
        param.setTaskId(taskId);
        do {
            Page<ArrangeResultByRoomVO> page = new GovPage<>();
            page.setSize(100);
            page.setCurrent(current);
            iArrangeService.searchArrangeResultByRoom(page, param);
            if(page.getRecords().size() > 0) {
                for (ArrangeResultByRoomVO record : page.getRecords()) {
                    List<Object> row = new ArrayList<>();
                    row.add(record.getRoomName());
                    row.add(record.getDept());
                    row.add(record.getArea());
                    row.add(record.getSeatNum());
                    for (Time time : timeList) {
                        Optional<ArrangeResultByRoomVO.Time> first = record.getTimeList().stream().filter(i -> time.getId().equals(i.getTimeId())).findFirst();
                        if(first.isPresent()) {
                            StringBuffer sb = new StringBuffer();
                            first.get().getSubjectList().stream().forEach(s -> {
                                sb.append(s.getSubjectName());
                                sb.append("(");
                                sb.append(s.getSubjectNum());
                                sb.append(")");
                            });
                            row.add(sb.toString());
                        } else {
                            row.add("");
                        }
                    }
                    data.add(row);
                }
            } else {
                break;
            }
            ++current;
        } while (true);
        return ImmutablePair.of(data, head);
    }

    private List<ArrangeResultBySubjectVO> getArrangeResultBySubject(String taskId) {
        List<ArrangeResultBySubjectVO> data = new ArrayList<>();
        int current = 1;
        ArrangeResultBySubjectParam param = new ArrangeResultBySubjectParam();
        param.setTaskId(taskId);
        do {
            Page<ArrangeResultBySubjectVO> page = new GovPage<>();
            page.setSize(100);
            page.setCurrent(current);
            iArrangeService.searchArrangeResultBySubject(page, param);
            if(page.getRecords().size() > 0) {
                data.addAll(page.getRecords());
            } else {
                break;
            }
            current++;
        } while (true);
        return data;
    }

    private List<ArrangeResultByTimeVO> getArrangeResultByTime(String taskId) {
        List<ArrangeResultByTimeVO> data = new ArrayList<>();
        ArrangeResultByTimeParam param = new ArrangeResultByTimeParam();
        param.setTaskId(taskId);

        int current = 1;
        do {
            Page<ArrangeResultByTimeVO> page = new GovPage<>();
            page.setSize(100);
            page.setCurrent(current);

            iArrangeService.searchArrangeResultByTime(page, param);
            if(page.getRecords().size() > 0) {
                data.addAll(page.getRecords());
            } else {
                break;
            }
            current++;
        } while (true);
        return data;
    }

    /**
     * 导出考生编排数据
     * 
     */
    @SneakyThrows
    @Override
    public void exportArrangeCandidate(ArrangeCandidateQueryParamVO arrangeCandidateQueryParamVO, String candidateIds) {
        // 任务id不能为空
        if (TextUtil.isNull(arrangeCandidateQueryParamVO.getTaskId())) {
            throw new KqException(ResponseCode.ARRANGE_EXPORT_PARAM_TASK_ID_CAN_NOT_NULL.getCode(),
                    ResponseCode.ARRANGE_EXPORT_PARAM_TASK_ID_CAN_NOT_NULL.getMsg());
        }
        String lastFileName = DateUtils.cfs(new Date()) + ".xlsx";
        File tempFile = FileUtil.createTempFile(StrPool.DOT + ".xlsx", Boolean.TRUE);
        tempFile.deleteOnExit();
        ExcelWriter excelWriter = null;
        try {
            ProgressManager.updateProgress(1L);

            excelWriter = EasyExcel.write(tempFile).build();

            List<ArrangeResultByTimeVO> arrangeResultByTime = getArrangeResultByTime(arrangeCandidateQueryParamVO.getTaskId());
            WriteSheet writeSheet1 = EasyExcel.writerSheet(0, "批次").head(ArrangeResultByTimeVO.class).build();
            excelWriter.write(arrangeResultByTime, writeSheet1);
            ProgressManager.updateProgress(30L);

            WriteSheet writeSheet2 = EasyExcel.writerSheet(1, "科目").head(ArrangeResultBySubjectVO.class).build();
            excelWriter.write(getArrangeResultBySubject(arrangeCandidateQueryParamVO.getTaskId()), writeSheet2);
            ProgressManager.updateProgress(60L);

            ImmutablePair<List<List<Object>>, List<List<String>>> arrangeResultByRoom = getArrangeResultByRoom(arrangeCandidateQueryParamVO.getTaskId());
            WriteSheet writeSheet3 = EasyExcel.writerSheet(2, "考场").head(arrangeResultByRoom.getRight()).build();
            excelWriter.write(arrangeResultByRoom.getLeft(), writeSheet3);
            ProgressManager.updateProgress(80L);

            List<List<String>> headList = this.headArrangeCandidateList();
            List<List<Object>> dataList = this.dataArrangeCandidateList(arrangeCandidateQueryParamVO, candidateIds);
            WriteSheet writeSheet4 = EasyExcel.writerSheet(3, "考生").head(headList)
                    .registerWriteHandler(new CustomCellWriteHandler()).build();
            excelWriter.write(dataList, writeSheet4);

            ProgressManager.updateProgress(95L);


        } finally {
            if(excelWriter != null) {
                excelWriter.finish();
            }
        }

        try {
            String path = "kw-gov/arrange/" + lastFileName;
            String fileUrl = cloudObsService.updateFile(path, tempFile);
            log.debug("# excel文件上传华为云成功：{}，{}", lastFileName, fileUrl);
            ProgressManager.updateProgressCurrent(100L, fileUrl);
        } finally {
            //tempFile.delete();
        }
    }

    @Override
    public void updateTaskDataLock(DataLockUpdateParam dataLockUpdateParam) throws KqException {
        Task task = iTaskService.getTaskById(dataLockUpdateParam.getTaskId());
        ProgressManager.updateProgress(6L);
        switch (TaskExamTypeEnum.getByCode(task.getExamType())) {
            // 线上机考删除数据
            case ONLINE:
                if (dataLockUpdateParam.getDataLock()) {
                    // 是否有编排的考生（强制要求）
                    this.checkArrange(dataLockUpdateParam.getTaskId());
                    ProgressManager.updateProgress(16L);
                    // 考生须知和欢迎语是否为空
                    iExamAgreementService.existNullExamAgreement(task.getId());
                    int total = timeRoomCandidateMapper.countPushTimeRoomCandidate(task.getId()) + StringUtil.getRandomInteger(100);
                    ProgressManager.updateProgress(22L);
                    // :先删除、再推送，临时方案防止数据重复
                    this.pushTimeRoomCandidateToKs(dataLockUpdateParam.getTaskId(), StringCodeEnum.PUSH_TYPE_DELETE.getCode(), null);
                    ProgressManager.updateProgress(75L);
                    // 推送数据到考试端，进度以该方法为主
                    this.pushTimeRoomCandidateToKs(dataLockUpdateParam.getTaskId(),
                            StringCodeEnum.PUSH_TYPE_INSERT.getCode(), total);
                    ProgressManager.updateProgress(90L);
                } else {
                    ProgressManager.updateProgress(23L);
                    //this.deleteByOnline(dataLockUpdateParam);
                    ProgressManager.updateProgress(66L);
                    // 删除考试端的编排数据
                    this.pushTimeRoomCandidateToKs(dataLockUpdateParam.getTaskId(),
                            StringCodeEnum.PUSH_TYPE_DELETE.getCode(), null);
                    ProgressManager.updateProgress(92L);
                }
                break;
            // 线下机考删除数据
            case OFFLINE:
                if (dataLockUpdateParam.getDataLock()) {
                    // 是否有编排的考生（强制要求）
                    this.checkArrange(dataLockUpdateParam.getTaskId());
                    ProgressManager.updateProgress(32L);
                    // 是否生成基础包（强制要求）
                    this.checkBaseData(dataLockUpdateParam.getTaskId());
                    ProgressManager.updateProgress(99L);
                } else {
                    this.deleteByOffline(dataLockUpdateParam);
                    ProgressManager.updateProgress(90L);
                }
                break;
        }
        // 更新数据锁定状态
        iTaskService.updateDataLock(dataLockUpdateParam.getTaskId(), dataLockUpdateParam.getDataLock());
        ProgressManager.updateProgress(99L);
    }

    /**
     * 线上机考解除锁定删除数据
     * 
     * @param dataLockUpdateParam 更改锁定状态参数
     */
    private void deleteByOnline(DataLockUpdateParam dataLockUpdateParam) {
        // 删除考务人员数据
        iExaminerService.deleteAllByTask(dataLockUpdateParam.getTaskId());
    }

    /**
     * 线下机考解除锁定删除数据
     * 
     * @param dataLockUpdateParam 更改锁定状态参数
     */
    private void deleteByOffline(DataLockUpdateParam dataLockUpdateParam) {
        // 删除考务包
        iTimeRoomService.deleteRoomDataByTaskId(dataLockUpdateParam.getTaskId());

    }

    /**
     *
     * @param taskId
     * @param type   新增：insert，删除：delete
     */
    private void pushTimeRoomCandidateToKs(String taskId, String type, Integer progress) {
        PushTimeRoomCandidateParam pushTimeRoomCandidateParam = new PushTimeRoomCandidateParam();
        pushTimeRoomCandidateParam.setTaskId(taskId).setPushType(type);
        if (StringCodeEnum.PUSH_TYPE_DELETE.getCode().equals(type)) {
            deleteKsTimeRoomCandidate(pushTimeRoomCandidateParam);
        } else {
            // 编排数据批量同步，一次最多同步200人。
            try {
                Integer maxSize = CodeEnum.PUSH_TIME_ROOM_CANDIDATE_MAX_SIZE.getCode();
                Integer dataIndex = 0;
                Integer currentSize = maxSize;
                ProgressManager.updateProgress(45L);
                while (currentSize.equals(maxSize)) {
                    List<PushTimeRoomCandidateVO> pushTimeRoomCandidateVOS = timeRoomCandidateMapper
                            .selectPushTimeRoomCandidate(taskId, dataIndex, maxSize);
                    if (pushTimeRoomCandidateVOS != null && !pushTimeRoomCandidateVOS.isEmpty()) {

                        log.info("# 推送ks编排数据：{}", JSON.toJSON(pushTimeRoomCandidateVOS));

                        pushTimeRoomCandidateParam.setPushTimeRoomCandidateVOS(pushTimeRoomCandidateVOS);
                        Long s = System.currentTimeMillis();
                        try {
                            ApiResponse<Object> res = iPushTimeRoomCandidateService
                                    .pushTimeRoomCandidate(pushTimeRoomCandidateParam);
                            log.info("# 推送ks数据结果：{}", FastJsonUtil.getBeanToJson(res));
                            if (res == null) {
                                throw new KqException(ResponseCode.TIME_ROOM_CANDIDATE_PUSH_KS_RESULT_NONE.getCode(),
                                        ResponseCode.TIME_ROOM_CANDIDATE_PUSH_KS_RESULT_NONE.getMsg());
                            }

                            if (!ResponseCode.SUCCESS.getCode().equals(res.getCode())) {
                                throw new KqException(res.getCode(), "调用考试接口失败。");
                            }

                        } finally {
                            Long e = System.currentTimeMillis();
                            log.info("# 推送ks数据完成：{},{}", e - s, pushTimeRoomCandidateParam.getTaskId());
                        }
                        currentSize = pushTimeRoomCandidateVOS.size();
                    } else {
                        currentSize = 0;
                    }
                    dataIndex += maxSize;
                }
            } catch (KqException e) {
                deleteKsTimeRoomCandidate(pushTimeRoomCandidateParam);
                log.info("编排数据推送异常，{}", taskId, e);
                throw e;
            } catch (Exception e) {
                deleteKsTimeRoomCandidate(pushTimeRoomCandidateParam);
                log.info("编排数据推送异常，{}", taskId, e);
                throw e;
            } finally {
                log.info("所有编排数据推送完成，{}", taskId);
            }
        }
    }

    /**
     * 删除考试编排数据
     * 
     * @param pushTimeRoomCandidateParam
     */
    public void deleteKsTimeRoomCandidate(PushTimeRoomCandidateParam pushTimeRoomCandidateParam) {
        // 删除调用一次就行
        pushTimeRoomCandidateParam.setPushType(StringCodeEnum.PUSH_TYPE_DELETE.getCode());
        ApiResponse<Object> res = iPushTimeRoomCandidateService.pushTimeRoomCandidate(pushTimeRoomCandidateParam);
        ProgressManager.updateProgress(65L);
        if (res == null) {
            throw new KqException(ResponseCode.TIME_ROOM_CANDIDATE_PUSH_KS_RESULT_NONE.getCode(),
                    ResponseCode.TIME_ROOM_CANDIDATE_PUSH_KS_RESULT_NONE.getMsg());
        }
        if (!ResponseCode.SUCCESS.getCode().equals(res.getCode())) {
            throw new KqException(res.getCode(), "调用考试接口失败。");
        }
    }

    /**
     * 检查是否有已编排考生
     * 
     * @param taskId 考试任务id
     * @throws KqException 暂无已编排的考生，无法锁定，抛出异常
     */
    private void checkArrange(String taskId) throws KqException {
        Integer arrangeCount = this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .count();
        if (arrangeCount <= 0) {
            // 暂无已编排的考生，无法锁定
            throw new KqException(ResponseCode.NO_CANDIDATES_HAVE_BEEN_ARRANGED.getCode(), "暂无已编排的考生，无法锁定");
        }
    }

    /**
     * 检查是否生成基础包
     * 
     * @param taskId 考试任务id
     * @throws KqException 暂未生成基础包，无法锁定，抛出异常
     */
    private void checkBaseData(String taskId) throws KqException {
        if (StringUtils.isEmpty(baseDataPackageMapper.getBaseDataPackageUrl(taskId))) {
            // 暂未生成基础包，无法锁定
            throw new KqException(ResponseCode.NOT_GENERATED_BASE_DATA.getCode(),
                    "暂未生成基础包，无法锁定");
        }
    }

    /**
     * 导出编排文件头部名称
     * 
     * @return
     */
    @Override
    public List<List<String>> headArrangeCandidateList() {

        List<List<String>> headList = new ArrayList<>();

        List<String> head0 = new ArrayList<>();
        head0.add("姓名");
        headList.add(head0);

        List<String> head1 = new ArrayList<>();
        head1.add("准考证号");
        headList.add(head1);

        List<String> head2 = new ArrayList<>();
        head2.add("性别");
        headList.add(head2);

        List<String> head3 = new ArrayList<>();
        head3.add("证件号码");
        headList.add(head3);

        List<String> head4 = new ArrayList<>();
        head4.add("报考科目");
        headList.add(head4);

        List<String> head5 = new ArrayList<>();
        head5.add("批次");
        headList.add(head5);

        List<String> head6 = new ArrayList<>();
        head6.add("考点");
        headList.add(head6);

        List<String> head7 = new ArrayList<>();
        head7.add("考场");
        headList.add(head7);

        List<String> head8 = new ArrayList<>();
        head8.add("座位号");
        headList.add(head8);

        List<String> head9 = new ArrayList<>();
        head9.add("开始考试时间");
        headList.add(head9);

        return headList;
    }

    /**
     * 导出编排文件数据
     * 
     * @param candidateIds
     * @return
     */
    @Override
    public List<List<Object>> dataArrangeCandidateList(ArrangeCandidateQueryParamVO arrangeCandidateQueryParamVO,
            String candidateIds) {

        List<TimeRoomCandidateVO> timeRoomCandidateVOList = timeRoomCandidateMapper
                .selectTimeRoomCandidateByCandidateIds(arrangeCandidateQueryParamVO, candidateIds);
        List<List<Object>> dataList = new ArrayList<>();
        timeRoomCandidateVOList.stream().forEach(timeRoomCandidateVO -> {
            List<Object> datas = new ArrayList<>();
            datas.add(timeRoomCandidateVO.getName());
            datas.add(timeRoomCandidateVO.getAdmissionNum());
            datas.add(timeRoomCandidateVO.getSex());
            datas.add(timeRoomCandidateVO.getIdNumber());
            datas.add(timeRoomCandidateVO.getSubjectName());
            datas.add(timeRoomCandidateVO.getTimeName());
            datas.add(timeRoomCandidateVO.getPlaceName());
            datas.add(timeRoomCandidateVO.getRoomName());
            datas.add(timeRoomCandidateVO.getSeatNum());
            datas.add(timeRoomCandidateVO.getStartTime());
            dataList.add(datas);
        });

        return dataList;
    }

    @Override
    public Boolean timeRoomCandidateExits(String taskId) {
        Integer count = this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .count();
        return count > 0;
    }

    // ==============================预览准考证号======================================

    /**
     * 预览准考证号
     * 
     * @param timeRoomParamVOList
     * @return
     */
    @Override
    public String getCandidateAdmission(List<TimeRoomParamVO> timeRoomParamVOList, String taskId) {
        Map<String, String> map = Maps.newHashMap();
        timeRoomParamVOList.forEach(timeRoomParamVO -> map.put(timeRoomParamVO.getCode(), timeRoomParamVO.getValue()));
        // 编排参数校验
        this.verifyArrangeParam(map);
        Integer admissionType = Integer.parseInt(String.valueOf(map.get("admissionType")));
        if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_GROUP.getCode())) {
            // 准考证号生成方式：自定义组合方式
            return viewAdmissionGroup(map, taskId);
        } else if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_MANUAL.getCode())) {
            // 准考证号生成方式：自定义手动方式
            return viewAdmissionManual(map, taskId);
        } else if (admissionType.equals(ArrangeParamCodeEnum.ADMISSION_TYPE_RANDOM.getCode())) {
            // 准考证号生成方式：随机方式
            return viewAdmissionRandom(map, taskId);
        } else if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
            // 自定义
            Optional<CustomArrange> optional = iCustomArrangeService.oneAdmissionNumberByTaskId(taskId);
            return optional.map(CustomArrange::getAdmissionNumber)
                    .orElseThrow(() -> new KqException(ResponseCode.ADMISSION_NUMBER_NOT_FOUND.getCode(),
                            ResponseCode.ADMISSION_NUMBER_NOT_FOUND.getMsg()));
        }
        return null;
    }

    /**
     * 预览准考证号
     * 
     * @return
     */
    private String viewAdmissionGroup(Map map, String taskId) {

        StringBuilder builder = new StringBuilder();
        String statusOn = "true";
        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_YEAR.getCode())).equals(statusOn)) {
            int groupYearDigit = Integer.parseInt(String.valueOf(map.get("groupYearDigit")));
            builder.append(DateTimeUtil.getFormatYear(groupYearDigit));
        }

        LambdaQueryWrapper<TaskArea> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(TaskArea::getTaskId, taskId)
                .eq(TaskArea::getType, 4);
        TaskArea taskArea = taskAreaService.list(queryWrapper).get(0);

        String[] areas = taskArea.getParentIds().split(",");
        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_PROVINCE.getCode())).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(map.get("groupProvinceDigit")));
            builder.append(StringUtil.stringRight(areas[0], digit));
        }

        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_CITY.getCode())).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(map.get("groupCityDigit")));
            builder.append(StringUtil.stringRight(areas[1], digit));
        }

        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_DISTRICT.getCode())).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(map.get("groupDistrictDigit")));
            builder.append(StringUtil.stringRight(taskArea.getAreaId(), digit));
        }

        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_RANDOM.getCode())).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(map.get("groupRandomDigit")));
            builder.append(StringUtil.stringFormatZero(new Random().nextInt(10), digit));
        }

        if (String.valueOf(map.get(StringCodeEnum.ARRANGE_PARAM_GROUP_AUTO.getCode())).equals(statusOn)) {
            int digit = Integer.parseInt(String.valueOf(map.get("groupAutoDigit")));
            builder.append(StringUtil.stringFormatZero(new Random().nextInt(10), digit));
        }
        return builder.toString();
    }

    /**
     * 预览准考证号
     * 
     * @return
     */
    private String viewAdmissionManual(Map map, String taskId) {
        StringBuilder builder = new StringBuilder();
        // 获取自定义前缀
        builder.append(map.get("manual"));
        // 获取自增序号位数
        int digit = Integer.parseInt(String.valueOf(map.get("manualIncreaseDigit")));
        // 获取随机数位数
        int randomNumberDigits = Integer
                .parseInt(map.get(StringCodeEnum.ARRANGE_PARAM_RANDOM_NUMBER_DIGIT.getCode()).toString());
        // 拼接随机数
        if (randomNumberDigits > 0) {
            builder.append(RandomUtil.randomNumbers(randomNumberDigits));
        }
        // 拼接
        builder.append(StringUtil.stringFormatZero(new Random().nextInt(10), digit));
        return builder.toString();
    }

    /**
     * 预览准考证号
     * 
     * @return
     */
    private String viewAdmissionRandom(Map map, String taskId) {
        String randomParam = String.valueOf(map.get("randomParam"));
        int digit = Integer.parseInt(String.valueOf(map.get("randomDigit")));
        return StringUtil.randomStringOrNumberNew(digit, randomParam);
    }

    @Override
    public ArrangeStatisticsVO arrangeStatistics(String taskId) {
        ArrangeStatisticsVO arrangeStatisticsVO = new ArrangeStatisticsVO();
        // 统计待编排考生数
        Integer arrangeCandidateCount = timeRoomCandidateMapper.arrangeCandidateStatistics(taskId);
        if (new Integer(0).equals(arrangeCandidateCount)) {
            // 暂无待编排考生
            return arrangeStatisticsVO;
        }
        // 统计已编排考生数
        Integer arrangedCandidateCount = timeRoomCandidateMapper.arrangedCandidateStatistics(taskId);
        if (new Integer(0).equals(arrangedCandidateCount)) {
            // 暂无已编排考生
            return arrangeStatisticsVO;
        }
        arrangeStatisticsVO.setArranged(arrangedCandidateCount);
        // 统计未编排考生数
        Integer unArrangedCandidateCount = arrangeCandidateCount - arrangedCandidateCount;
        arrangeStatisticsVO.setUnArranged(unArrangedCandidateCount);
        // 统计已编排考场数
        List<String> arrangedRoomIdList = timeRoomCandidateMapper.arrangedRoomIdsStatistics(taskId);
        arrangeStatisticsVO.setRoom(arrangedRoomIdList.size());
        // 统计已编排考点数
        Integer arrangedPlaceCount = iTaskRoomService.lambdaQuery()
                .select(TaskRoom::getPlaceId)
                .eq(TaskRoom::getDelFlag, false)
                .in(TaskRoom::getId, arrangedRoomIdList)
                .groupBy(TaskRoom::getPlaceId)
                .list()
                .size();
        arrangeStatisticsVO.setPlace(arrangedPlaceCount);
        timeRoomExaminerCacheService.clearUserProctorCache(null, taskId);
        return arrangeStatisticsVO;
    }

    @Override
    public List<Seat> getSeatList(String taskId, String timeRoomId) {
        return timeRoomCandidateMapper.getSeatList(taskId, timeRoomId);
    }

    @Override
    public List<GceSeat> getGceSeatList(String taskId, String timeRoomId) {
        return timeRoomCandidateMapper.getGceSeatList(taskId, timeRoomId);
    }

    @Override
    public List<ObtCandidate> getCandidateList(String taskId, String timeRoomId) {
        return timeRoomCandidateMapper.getCandidateList(taskId, timeRoomId);
    }

    @Override
    public List<GceCandidate> getGceCandidateList(String taskId, String timeRoomId) {
        return timeRoomCandidateMapper.getGceCandidateList(taskId, timeRoomId);
    }

    @Override
    public List<TimeRoomCandidateVO> selectCandidateInfo(String taskId, String candidateId) {
        return timeRoomCandidateMapper.selectCandidateInfo(taskId, candidateId);
    }

    @Override
    public Integer countArrangedCandidate(String taskId) {
        return this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .count();
    }

    @Override
    public List<TimeRoomCandidate> listByTaskId(String taskId) {
        return this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .list();
    }

    @Override
    public String exportArrangeResultDBF(String taskId) throws Exception {
        File root = new File(configManager.getDbfRoot(), UUIDUtils.newSortUUID());
        try {
            log.info("# 考场编排数据导出路径:{}", root.getAbsolutePath());
            root.mkdirs();
            // 异步执行dbf文件生成操作
            arrangedResultExportUtil.creatKsDbf(taskId, root);
            arrangedResultExportUtil.creatKdDbf(taskId, root);
            arrangedResultExportUtil.creatKpDbf(taskId, root);
            arrangedResultExportUtil.creatKqDbf(taskId, root);
            // arrangedResultExportUtil.creatCandidatePhoto(taskId, root);

            // 打包并上传至华为云OBS
            return this.packageUpload(taskId, root);
        } finally {
            // 删除临时文件
            if (root != null) {
                try {
                    FileUtils.deleteDirectory(root);
                } catch (Exception e) {
                    log.error("删除dbf临时文件失败，临时文件所在本地路径:{}", root.getAbsolutePath());
                }

            }
        }
    }

    private String packageUpload(String taskId, File folderDir) throws Exception {
        File dbfPackage = null;
        try {
            // 创建zip包临时文件
            dbfPackage = new File(configManager.getDbfRoot() + UUIDUtils.newSortUUID() + ".zip");
            dbfPackage.getParentFile().mkdirs();
            log.info("# 编排结果dbf打包文件所在路径: {}", dbfPackage.getAbsolutePath());
            ZipUtil.zipFolderNotIncludeRootFolderWithPassword(dbfPackage.getAbsolutePath(), folderDir, "123456");
            String path = configManager.getCloudRootPath() + "dbf/" + taskId + "/" + dbfPackage.getName();
            // 上传至华为云OBS
            String url = cloudObsService.updateFile(path, new FileInputStream(dbfPackage.getAbsolutePath()));
            SysFile sysFile = new SysFile();
            sysFile.setUrl(url);
            sysFile.setPath(path);
            sysFile.setName(dbfPackage.getName());
            sysFile.setType(FileTypeEnum.FILE_DBF_PACKAGE.getName());
            sysFile.setSize(dbfPackage.length());
            // 保存文件信息
            iSysFileService.save(sysFile);
            return url;
        } finally {
            if (dbfPackage != null) {
                if (!dbfPackage.delete()) {
                    log.error("#删除编排结果dbf压缩包临时文件失败，临时文件目录: {}",
                            dbfPackage.getAbsolutePath());
                }
            }
        }
    }

    @Override
    public void deleteAllArranged(String taskId) {
        QueryWrapper<TimeRoomCandidate> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getDelFlag, false);
        remove(queryWrapper);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pushSimpleCandidateToKs(String taskId, String candidateId, String type) {
        PushTimeRoomCandidateParam pushTimeRoomCandidateParam = new PushTimeRoomCandidateParam();
        pushTimeRoomCandidateParam.setTaskId(taskId).setPushType(type);

        List<PushTimeRoomCandidateVO> pushTimeRoomCandidateVOS = timeRoomCandidateMapper
                .selectPushTimeRoomCandidateForSimple(taskId, candidateId);
        pushTimeRoomCandidateParam.setPushTimeRoomCandidateVOS(pushTimeRoomCandidateVOS);

        ApiResponse<Object> res = ksClient.pushCandidate(pushTimeRoomCandidateParam);
        if (res == null) {
            throw new KqException(ResponseCode.PUSH_SIMPLE_CANDIDATE_ERROR.getCode(),
                    ResponseCode.PUSH_SIMPLE_CANDIDATE_ERROR.getMsg());
        }
        if (!ResponseCode.SUCCESS.getCode().equals(res.getCode())) {
            throw new KqException(res.getCode(), "调用考试接口失败。");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importTimeRoomCandidate(String taskId, MultipartFile file) {
        // Task task = iTaskService.getTaskById(taskId);

        List<TimeRoomCandidateXySheet> timeRoomCandidateXySheetList = readExcel(file);

        // 考生，考生科目，科目，批次，批次科目，考场，考点
        Map<String, String> temMap = getData(taskId);
        temMap.put("auto", "1");

        Long s = System.currentTimeMillis();
        timeRoomCandidateXySheetList.stream().forEach(timeRoomCandidateXySheet -> {

            // 考生存在，则不需要处理
            if (temMap.containsKey(timeRoomCandidateXySheet.getIdNumber())) {
                return;
            }

            // 1.考生信息
            Candidate candidate = new Candidate();
            candidate.setName(timeRoomCandidateXySheet.getName())
                    .setIdNumber(timeRoomCandidateXySheet.getIdNumber())
                    .setIdType(0)
                    .setTaskId(taskId)
                    .setDelFlag(false)
                    .setId(UUIDUtils.newSortUUID());
            iCandidateService.save(candidate);

            // 2.科目信息
            if (!temMap.containsKey(timeRoomCandidateXySheet.getSubjectName())) {
                String subjectId = UUIDUtils.newSortUUID();
                Subject subject = new Subject();
                subject.setName(timeRoomCandidateXySheet.getSubjectName())
                        .setTaskId(taskId)
                        .setCode(temMap.get("auto"))
                        .setLength(new BigDecimal(1))
                        .setHandInTime(new BigDecimal(1))
                        .setDelFlag(false)
                        .setId(subjectId);
                temMap.put(timeRoomCandidateXySheet.getSubjectName(), subjectId);
                iSubjectService.save(subject);

                // 创建批次,场次：每个科目对应一个批次
                createTimeBySubject(subject, temMap);

                Integer i = Integer.parseInt(temMap.get("auto")) + 1;
                temMap.put("auto", i + "");
            }

            // 3.考生科目

            CandidateSubject candidateSubject = new CandidateSubject();
            candidateSubject.setCandidateId(candidate.getId())
                    .setTaskId(taskId)
                    .setSubjectId(temMap.get(timeRoomCandidateXySheet.getSubjectName()))
                    .setAnswerType(1)
                    .setLanguage(1)
                    .setDelFlag(false)
                    .setId(UUIDUtils.newSortUUID());
            iCandidateSubjectService.save(candidateSubject);

            // 考点
            if (!temMap.containsKey(timeRoomCandidateXySheet.getPlaceName())) {
                TaskPlace taskPlace = new TaskPlace();
                taskPlace.setTaskId(taskId)
                        .setName(timeRoomCandidateXySheet.getPlaceName())
                        .setAddress(timeRoomCandidateXySheet.getPlaceAddress())
                        .setDelFlag(false)
                        .setId(UUIDUtils.newSortUUID());
                temMap.put(timeRoomCandidateXySheet.getPlaceName(), taskPlace.getId());
                iTaskPlaceService.save(taskPlace);
            }

            // 考场
            String roomKey = StringUtils.join(timeRoomCandidateXySheet.getPlaceName(), "_",
                    timeRoomCandidateXySheet.getRoomName());
            if (!temMap.containsKey(roomKey)) {
                TaskRoom taskRoom = new TaskRoom();
                taskRoom.setTaskId(taskId)
                        .setName(timeRoomCandidateXySheet.getRoomName())
                        .setAnswerType(1)
                        .setPlaceId(temMap.get(timeRoomCandidateXySheet.getPlaceName()))
                        .setAddress(timeRoomCandidateXySheet.getRoomAddress())
                        .setDelFlag(false)
                        .setId(UUIDUtils.newSortUUID());
                temMap.put(roomKey, taskRoom.getId());
                iTaskRoomService.save(taskRoom);

                // 创建场次
            }

            // 编排
            String key = StringUtils.join(timeRoomCandidateXySheet.getSubjectName(), "_pcId");
            TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
            timeRoomCandidate.setCandidateId(candidate.getId())
                    .setCandidateSubjectId(candidateSubject.getId())
                    .setRoomId(temMap.get(roomKey))
                    .setTimeId(temMap.get(key))
                    .setSeatNum(timeRoomCandidateXySheet.getSeatNum())
                    .setAdmissionNum(timeRoomCandidateXySheet.getAdmissionNum())
                    .setSubjectId(temMap.get(timeRoomCandidateXySheet.getSubjectName()))
                    .setTaskId(taskId)
                    .setDelFlag(false)
                    .setId(UUIDUtils.newSortUUID());

            this.save(timeRoomCandidate);
        });

        // 生成场次id
        createTimeRoom(taskId);

        Long e = System.currentTimeMillis();
        log.info("生成数据完成，耗时{}", (e - s));
    }

    @SneakyThrows
    private List<TimeRoomCandidateXySheet> readExcel(MultipartFile file) {

        TimeRoomCandidateXyListener timeRoomCandidateXyListener = new TimeRoomCandidateXyListener();
        EasyExcel.read(file.getInputStream(), TimeRoomCandidateXySheet.class, timeRoomCandidateXyListener)
                .sheet(1).doReadSync();

        return timeRoomCandidateXyListener.getTimeRoomCandidateList();
    }

    private Map<String, String> getData(String taskId) {

        Map<String, String> temMap = new HashMap<>();

        Map<String, String> temMap2 = new HashMap<>();

        // 1.查询考生
        List<Candidate> candidateList = iCandidateService.lambdaQuery()
                .eq(Candidate::getTaskId, taskId)
                .eq(Candidate::getDelFlag, false)
                .select(Candidate::getIdNumber, Candidate::getId).list();
        candidateList.stream().forEach(candidate -> {
            temMap.put(candidate.getIdNumber(), candidate.getId());
            temMap2.put(candidate.getId(), candidate.getIdNumber());
        });

        // 2.查询科目
        List<Subject> subjectList = iSubjectService.lambdaQuery()
                .eq(Subject::getTaskId, taskId)
                .eq(Subject::getDelFlag, false)
                .list();
        subjectList.stream().forEach(subject -> {
            temMap.put(subject.getName(), subject.getId());
            temMap2.put(subject.getId(), subject.getName());
        });

        // 3.查询考生科目
        List<CandidateSubject> candidateSubjectList = iCandidateSubjectService.lambdaQuery()
                .eq(CandidateSubject::getTaskId, taskId)
                .eq(CandidateSubject::getDelFlag, false)
                .list();
        candidateSubjectList.stream().forEach(candidateSubject -> {
            String key = StringUtils.join(temMap2.get(candidateSubject.getCandidateId()), "_",
                    temMap2.get(candidateSubject.getSubjectId()));
            temMap.put(key, candidateSubject.getId());
        });

        // 4.考点
        List<TaskPlace> taskPlaces = iTaskPlaceService.lambdaQuery()
                .eq(TaskPlace::getTaskId, taskId)
                .eq(TaskPlace::getDelFlag, false)
                .list();
        taskPlaces.stream().forEach(taskPlace -> {
            temMap.put(taskPlace.getName(), taskPlace.getId());
            temMap2.put(taskPlace.getId(), taskPlace.getName());
        });

        // 5.考场
        List<TaskRoom> taskRoomList = iTaskRoomService.lambdaQuery()
                .eq(TaskRoom::getTaskId, taskId)
                .eq(TaskRoom::getDelFlag, false)
                .list();
        taskRoomList.stream().forEach(taskRoom -> {
            String key = StringUtils.join(temMap2.get(taskRoom.getPlaceId()), "_", taskRoom.getName());
            temMap.put(key, taskRoom.getId());
        });

        // 6.批次和科目
        List<TimeSubject> timeSubjects = iTimeSubjectService.lambdaQuery()
                .eq(TimeSubject::getTaskId, taskId)
                .eq(TimeSubject::getDelFlag, false)
                .list();
        timeSubjects.stream().forEach(timeSubject -> {
            // 科目名称+批次id
            String key = StringUtils.join(temMap2.get(timeSubject.getSubjectId()), "_pcId");
            temMap.put(key, timeSubject.getTimeId());
        });

        return temMap;
    }

    private void createTimeBySubject(Subject subject, Map<String, String> temMap) {

        Time time = new Time();
        time.setTaskId(subject.getTaskId())
                .setName(subject.getName())
                .setCode(Integer.parseInt(subject.getCode()))
                .setStartTime(DateTimeUtil.getFormatDateTime("2022-01-08 15:00:00"))
                .setEndTime(DateTimeUtil.getFormatDateTime("2022-01-08 16:00:00"))
                .setDelFlag(false)
                .setId(UUIDUtils.newSortUUID());
        iTimeService.save(time);

        TimeSubject timeSubject = new TimeSubject();
        timeSubject.setSubjectId(subject.getId())
                .setTimeId(time.getId())
                .setTaskId(subject.getTaskId())
                .setDelFlag(false)
                .setId(UUIDUtils.newSortUUID());
        iTimeSubjectService.save(timeSubject);

        String key = StringUtils.join(subject.getName(), "_pcId");
        temMap.put(key, time.getId());
    }

    private void createTimeRoom(String taskId) {
        List<TimeRoomCandidate> candidateList = timeRoomCandidateMapper.selectTimeRoomByTimeRoomIdNull(taskId);

        List<TimeRoom> timeRoomList = null;
        Map<String, String> temMap = new HashMap<>();
        if (!candidateList.isEmpty()) {
            timeRoomList = iTimeRoomService.lambdaQuery()
                    .eq(TimeRoom::getTaskId, taskId)
                    .eq(TimeRoom::getDelFlag, false)
                    .list();

            timeRoomList.stream().forEach(timeRoom -> {
                String key = StringUtils.join(timeRoom.getTimeId(), "_", timeRoom.getRoomId());
                temMap.put(key, timeRoom.getId());
            });

            candidateList.forEach(timeRoomCandidate -> {
                //
                String key = StringUtils.join(timeRoomCandidate.getTimeId(), "_", timeRoomCandidate.getRoomId());
                if (!temMap.containsKey(key)) {
                    // 如果场次不存在，则新增场次
                    TimeRoom timeRoom = new TimeRoom();
                    timeRoom.setTimeId(timeRoomCandidate.getTimeId())
                            .setRoomId(timeRoomCandidate.getRoomId())
                            .setTaskId(taskId)
                            .setDelFlag(false)
                            .setId(UUIDUtils.newSortUUID());
                    iTimeRoomService.save(timeRoom);
                    temMap.put(key, timeRoom.getId());
                }
                // 更新编排表中的场次id
                timeRoomCandidateMapper.updateTimeRoomByTimeRoomIdNull(taskId, temMap.get(key),
                        timeRoomCandidate.getTimeId(), timeRoomCandidate.getRoomId());

            });
        }

    }

    @Override
    @Transactional
    public void pushWeChatArrangedNotification(String taskId) throws Exception {
        // 校验是否锁定编排数据
        iTaskService.isNotLockDataThrowException(taskId);
        // 获取已编排考生微信通知
        try (Cursor<PushExamNotifyDTO> cursor = timeRoomCandidateMapper.getArrangedWeChatNotificationVO(taskId)) {
            // 分批迭代已编排考生微信通知
            Iterator<PushExamNotifyDTO> iterator = cursor.iterator();
            int batchSize = configManager.getArrangedNotificationBatchSize().intValue();
            List<PushExamNotifyDTO> batch = new ArrayList<>(batchSize);
            while (iterator.hasNext()) {
                for (int i = 0; i < batchSize && iterator.hasNext(); i++) {
                    batch.add(iterator.next());
                }
                // 推送已编排考生微信通知至报名系统
                this.pushArrangedWeChatNotificationBatch(batch);
                // 当前批通知推送成功，清除当前批通知数据防止OOM，然后继续推送下一批通知，直至所有通知推送完毕
                batch.clear();
            }
        }
    }

    @Override
    public NowTestNowDataVO getCandidateArrange(String taskId, String timeId) {
        // 获取任务数据
        Task task = iTaskService.getTaskById(taskId);

        NowTestNowDataVO nowTestNowDataVO = new NowTestNowDataVO();
        nowTestNowDataVO.setBeginDate(DateTimeUtil.getDateTimeString(task.getBeginDate()));
        nowTestNowDataVO.setEndDate(DateTimeUtil.getDateTimeString(task.getEndDate()));
        nowTestNowDataVO.setExamName(task.getName());
        nowTestNowDataVO.setId(taskId);

        /*
         * String[] testClassifys = task.getTestClassify().split(",");
         * int length = testClassifys.length-1;
         * String testClassifysId = testClassifys[length];
         */
        List<TestClassifyVO> testClassifyVOList = iTaskService.getTestClassify();
        setTestClassify(testClassifyVOList, task.getTestClassify(), nowTestNowDataVO);
        nowTestNowDataVO.setTestClassify(task.getTestClassify());

        /*
         * TestClassifyVO testClassifyVOObj =
         * testClassifyVOList.stream().filter(testClassifyVO ->
         * testClassifyVO.getId().equals(testClassifysId)).findFirst().orElse(null);
         * nowTestNowDataVO.setTestClassifyName(testClassifyVOObj.getName());
         * nowTestNowDataVO.setTestClassify(task.getTestClassify());
         */

        // 获取考生编排数据
        List<TimeRoomCandidate> timeRoomCandidateList = this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getTimeId, timeId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .list();

        Map<String, TaskRoom> mapTaskRoom = iTaskRoomService.mapByTaskId(taskId);
        Map<String, TaskPlace> mapTaskPlace = iTaskPlaceService.mapByTaskId(taskId);
        Map<String, TaskArea> mapTaskArea = taskAreaService.getTaskAreaMap(taskId);

        List<NowTestNowCandidateArrangeVO> nowTestNowCandidateArrangeVOList = new ArrayList<>();
        timeRoomCandidateList.stream().forEach(timeRoomCandidate -> {
            NowTestNowCandidateArrangeVO nowTestNowCandidateArrangeVO = new NowTestNowCandidateArrangeVO();
            nowTestNowCandidateArrangeVO.setCandidateId(timeRoomCandidate.getCandidateId());
            nowTestNowCandidateArrangeVO.setSeatNo(timeRoomCandidate.getSeatNum());

            Candidate candidate = iCandidateService.getCandidateByCandidateId(taskId,
                    timeRoomCandidate.getCandidateId());
            if (StringUtils.isNotBlank(candidate.getProvince())) {
                nowTestNowCandidateArrangeVO.setProvinceCode(candidate.getProvince());
                Optional<TaskArea> ot = Optional.ofNullable(mapTaskArea.get(candidate.getProvince()));
                nowTestNowCandidateArrangeVO.setProvinceName(ot.isPresent() ? ot.get().getName() : "");
            }

            if (StringUtils.isNotBlank(candidate.getCity())) {
                nowTestNowCandidateArrangeVO.setCityCode(candidate.getCity());
                Optional<TaskArea> ot = Optional.ofNullable(mapTaskArea.get(candidate.getCity()));
                nowTestNowCandidateArrangeVO.setCityName(ot.isPresent() ? ot.get().getName() : "");
            }

            if (StringUtils.isNotBlank(candidate.getDistrict())) {
                nowTestNowCandidateArrangeVO.setDistrictCode(candidate.getDistrict());
                Optional<TaskArea> ot = Optional.ofNullable(mapTaskArea.get(candidate.getProvince()));
                nowTestNowCandidateArrangeVO.setDistrictName(ot.isPresent() ? ot.get().getName() : "");
            }

            TaskRoom taskRoom = mapTaskRoom.get(timeRoomCandidate.getRoomId());
            nowTestNowCandidateArrangeVO.setRoomId(taskRoom.getId());
            nowTestNowCandidateArrangeVO.setRoomName(taskRoom.getName());

            TaskPlace taskPlace = mapTaskPlace.get(taskRoom.getPlaceId());
            nowTestNowCandidateArrangeVO.setPlaceId(taskPlace.getId());
            nowTestNowCandidateArrangeVO.setPlaceName(taskPlace.getName());

            nowTestNowCandidateArrangeVOList.add(nowTestNowCandidateArrangeVO);
        });

        nowTestNowDataVO.setNowTestNowCandidateArrangeVOList(nowTestNowCandidateArrangeVOList);
        return nowTestNowDataVO;
    }

    private void setTestClassify(List<TestClassifyVO> testClassifyVOList, String taskTestClassify,
            NowTestNowDataVO nowTestNowDataVO) {
        testClassifyVOList.stream().forEach(testClassifyVO -> {
            if (taskTestClassify.contains(testClassifyVO.getId())) {
                if (StringUtils.isBlank(nowTestNowDataVO.getTestClassifyName())) {
                    nowTestNowDataVO.setTestClassifyName(testClassifyVO.getName());
                } else {
                    nowTestNowDataVO.setTestClassifyName(
                            StringUtils.join(nowTestNowDataVO.getTestClassifyName(), ",", testClassifyVO.getName()));
                }

                if (testClassifyVO.getChildren() != null && !testClassifyVO.getChildren().isEmpty()) {
                    setTestClassify(testClassifyVO.getChildren(), taskTestClassify, nowTestNowDataVO);
                }
            }
        });
    }

    /**
     * 获取任务考试类型及名称
     * 
     * @param taskId
     * @return
     */
    @Override
    public NowTestNowDataVO getTaskExamType(String taskId) {
        // 获取任务数据
        Task task = iTaskService.getTaskById(taskId);
        NowTestNowDataVO nowTestNowDataVO = new NowTestNowDataVO();
        nowTestNowDataVO.setBeginDate(DateTimeUtil.getDateTimeString(task.getBeginDate()));
        nowTestNowDataVO.setEndDate(DateTimeUtil.getDateTimeString(task.getEndDate()));
        nowTestNowDataVO.setExamName(task.getName());
        nowTestNowDataVO.setId(taskId);

        nowTestNowDataVO.setTestClassify(task.getTestClassify());
        List<TestClassifyVO> testClassifyVOList = iTaskService.getTestClassify();
        setTestClassify(testClassifyVOList, task.getTestClassify(), nowTestNowDataVO);
        /*
         * //去获取考试分类名称
         * 
         * TestClassifyVO testClassifyVOObj =
         * testClassifyVOList.stream().filter(testClassifyVO ->
         * testClassifyVO.getId().equals(testClassifysId)).findFirst().orElse(null);
         * nowTestNowDataVO.setTestClassifyName(testClassifyVOObj.getName());
         */
        return nowTestNowDataVO;
    }

    /**
     * 推送已编排考生微信通知至报名系统
     * 
     * @param batch 当前批通知数据
     */
    private void pushArrangedWeChatNotificationBatch(List<PushExamNotifyDTO> batch) {
        // 调用报名推送通知接口，推送一批通知
        ApiResponse<?> response = bmClient.pushExamNotify(batch);
        // 判断当前批通知是否推送成功
        if (!ResponseCode.SUCCESS.getCode().equals(response.getCode())
                && !CodeEnum.BM_SUCCESS_CODE.getCode().equals(response.getCode())) {
            // 当前批通知推送成功则继续推送下一批通知，反之抛出异常
            throw new KqException(ResponseCode.PUSH_ARRANGED_WECHAT_NOTIFICATION_BATCH_EXCEPTION.getCode(),
                    "推送已编排考生微信通知至报名系统异常");
        }
        log.info("调用报名推送通知接口，推送一批数量为 {} 的通知成功", batch.size());
    }

    @Override
    public List<WeChatCandidateArrangeVO> getWeChatCandidateArrangeVO(String taskSignUpId, String candidateSignUpId) {

        if (StringUtils.isBlank(taskSignUpId) || StringUtils.isBlank(candidateSignUpId)) {
            return null;
        }

        // 根据报名id获取考试任务id
        String taskId = iTaskService.getIdBySignUpId(taskSignUpId);
        if (StringUtils.isBlank(taskId)) {
            return null;
        }

        // 获取微信考生考试安排VO
        return timeRoomCandidateMapper.getWeChatCandidateArrangeVO(taskId, candidateSignUpId);
    }

    @Override
    public List<WeChatCandidateArrangeVO> getWeChatCandidateArrangeVO(String candidateSignUpIds) {

        if (StringUtils.isBlank(candidateSignUpIds)) {
            return null;
        }

        List<Candidate> candidates = iCandidateService.getCandidateByCandidateBmIds(candidateSignUpIds);
        if (candidates == null || CollectionUtils.isEmpty(candidates)) {
            return null;
        }

        List<WeChatCandidateArrangeVO> weChatCandidateArrangeVOList = new ArrayList<>();

        candidates.stream().forEach(candidate -> {
            // 获取编排数据
            List<WeChatCandidateArrangeVO> weChatCandidateArrangeVOs = this.timeRoomCandidateMapper
                    .getWeChatCandidateArrangeVOByCandidateId(candidate.getId());
            if (weChatCandidateArrangeVOs == null || CollectionUtils.isEmpty(weChatCandidateArrangeVOs)) {
                return;
            }
            weChatCandidateArrangeVOs.stream().forEach(weChatCandidateArrangeVO -> {

                // 1.获取考场类型
                String roomType = iTimeRoomParamService.getTaskTimeRoomParamValueByCode(
                        weChatCandidateArrangeVO.getTaskId(), StringCodeEnum.ARRANGE_PARAM_ROOM_TYPE.getCode());
                roomType = StringUtils.isBlank(roomType) ? "0" : roomType;
                weChatCandidateArrangeVO.setRoomType(Integer.parseInt(roomType));

                // 准考证地址, 首先获取准考证号码
                String admissionNumber = weChatCandidateArrangeVO.getAdmissionNumber();
                if (StringUtils.isNoneBlank(admissionNumber)) {
                    ExamineeTimes examineeTimes = iExamineeTimesService
                            .getByTimeAndAdmissinNo(weChatCandidateArrangeVO.getTimeId(), admissionNumber);
                    if (null != examineeTimes && StringUtils.isNotEmpty(examineeTimes.getAdmissionTicketPdfUrl())) {
                        weChatCandidateArrangeVO.setTicket(examineeTimes.getAdmissionTicketPdfUrl());
                    }
                }

                // 3.考试链接地址
                ApiResponse<?> response = ksClient.getExamUrl(weChatCandidateArrangeVO.getTimeId());
                String examUrl = Optional.ofNullable(response).map(resp -> {
                    if (ResponseCode.SUCCESS.getCode().equals(resp.getCode())
                            && ObjectUtils.isNotEmpty((resp.getData()))
                            && String.class.equals(resp.getData().getClass())) {
                        return String.valueOf(resp.getData());
                    }
                    return null;
                }).orElse(null);
                weChatCandidateArrangeVO.setExamUrl(examUrl);
            });

            weChatCandidateArrangeVOList.addAll(weChatCandidateArrangeVOs);
        });

        return weChatCandidateArrangeVOList;
    }

    @Override
    public WeChatCandidateArrangeDetailVO getWeChatCandidateArrangeDetailVO(String taskSignUpId, String timeId,
            String candidateSignUpId) {

        if (StringUtils.isBlank(taskSignUpId) || StringUtils.isBlank(timeId)
                || StringUtils.isBlank(candidateSignUpId)) {
            return null;
        }

        List<Candidate> candidates = iCandidateService.getCandidateByCandidateBmIds(candidateSignUpId);
        if (candidates == null || CollectionUtils.isEmpty(candidates)) {
            return null;
        }

        Candidate candidate = candidates.get(0);

        // 获取编排数据
        WeChatCandidateArrangeDetailVO weChatCandidateArrangeVO = this.timeRoomCandidateMapper
                .getWeChatCandidateArrangeDetailVOByTaskBmTimeCandidate(taskSignUpId, timeId, candidate.getId());
        if (weChatCandidateArrangeVO == null) {
            return null;
        }

        // 1.获取考场类型
        String roomType = iTimeRoomParamService.getTaskTimeRoomParamValueByCode(candidate.getTaskId(),
                StringCodeEnum.ARRANGE_PARAM_ROOM_TYPE.getCode());
        roomType = StringUtils.isBlank(roomType) ? "0" : roomType;
        weChatCandidateArrangeVO.setRoomType(Integer.parseInt(roomType));

        // 2.准考证地址
        String admissionNumber = weChatCandidateArrangeVO.getAdmissionNumber();
        if (StringUtils.isNoneBlank(admissionNumber)) {
            ExamineeTimes examineeTimes = iExamineeTimesService.getByTimeAndAdmissinNo(timeId, admissionNumber);
            if (null != examineeTimes && StringUtils.isNotEmpty(examineeTimes.getAdmissionTicketPdfUrl())) {
                weChatCandidateArrangeVO.setTicket(examineeTimes.getAdmissionTicketPdfUrl());
            }
        }

        // CandidateTicket candidateTicket =
        // iCandidateTicketService.viewCandidateAdmission(candidate.getTaskId(),candidate.getId());
        // if (candidateTicket != null) {
        // WeChatCandidateArrangeVO.setTicket(candidateTicket.getPath());
        // }

        // 3.考试链接地址
        ApiResponse<?> response = ksClient.getExamUrl(timeId);
        String examUrl = Optional.ofNullable(response).map(resp -> {
            if (ResponseCode.SUCCESS.getCode().equals(resp.getCode())
                    && ObjectUtils.isNotEmpty((resp.getData()))
                    && String.class.equals(resp.getData().getClass())) {
                return String.valueOf(resp.getData());
            }
            return null;
        }).orElse(null);
        weChatCandidateArrangeVO.setExamUrl(examUrl);

        return weChatCandidateArrangeVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void edit(ArrangedEditParam arrangedEditParam) {
        // 校验编排数据是否已经锁定，已锁定不允许操作
        iTaskService.isLockDataThrowException(arrangedEditParam.getTaskId());
        // 获取所有需要修改的编排数据
        List<TimeRoomCandidate> beingEditList = timeRoomCandidateMapper.getBeingEdit(arrangedEditParam);
        // 获取修改到目标考场的场次信息
        List<TimeRoom> destinationTimeRoomList = iTimeRoomService.listByTaskRoomId(arrangedEditParam.getTaskId(),
                arrangedEditParam.getRoomId());
        // 获取修改到目标考场的考生编排数据
        List<TimeRoomCandidate> destinationList = this.listByTaskRoomId(arrangedEditParam.getTaskId(),
                arrangedEditParam.getRoomId());
        // 校验修改编排结果数据
        this.verifyBeingEditData(arrangedEditParam, beingEditList, destinationList, destinationTimeRoomList);
        // 组装迁移考场后的编排信息
        this.convertTransferredData(beingEditList, destinationTimeRoomList);
        // 更新编排数据
        this.updateBatchById(beingEditList);
    }

    /**
     * 校验修改编排结果数据
     * 
     * @param arrangedEditParam       修改编排结果参数
     * @param beingEditList           所有需要修改的编排数据
     * @param destinationList         修改到目标考场的考生编排数据
     * @param destinationTimeRoomList 修改到目标考场的场次信息
     */
    private void verifyBeingEditData(ArrangedEditParam arrangedEditParam,
            List<TimeRoomCandidate> beingEditList,
            List<TimeRoomCandidate> destinationList,
            List<TimeRoom> destinationTimeRoomList) {

        // 校验修改编排结果数据是否存在
        if (CollectionUtils.isEmpty(beingEditList)) {
            // 待修改的考生编排数据不能为空
            throw new KqException(ResponseCode.ARRANGED_EDIT_DATA_NOT_FOUND.getCode(),
                    ResponseCode.ARRANGED_EDIT_DATA_NOT_FOUND.getMsg());
        }
        // 校验编排结果待修改到的考场是否有场次数据
        if (CollectionUtils.isEmpty(destinationTimeRoomList)) {
            // 编排结果将要修改到的目标考场暂无场次数据
            throw new KqException(ResponseCode.TIME_DATA_NOT_FOUND_FOR_THE_DESTINATION_ROOM.getCode(),
                    ResponseCode.TIME_DATA_NOT_FOUND_FOR_THE_DESTINATION_ROOM.getMsg());
        }

        // 批量选中编排数据的情况下，校验选中的编排数据是否为同一批次下的编排数据
        if (StringUtils.isNotBlank(arrangedEditParam.getIds())) {
            long distinctCount = beingEditList.stream()
                    .map(TimeRoomCandidate::getTimeId)
                    .distinct()
                    .count();
            if (distinctCount != 1) {
                // 选中的编排数据必须为同一批次下的编排数据
                throw new KqException(ResponseCode.ARRANGED_EDIT_DATA_MUST_BE_THE_SAME_TIME.getCode(),
                        ResponseCode.ARRANGED_EDIT_DATA_MUST_BE_THE_SAME_TIME.getMsg());
            }
        }

        // 校验将要修改到的目标考场中是否已关联所有需要修改编排数据的考生的批次数据
        List<String> timeIdList = beingEditList.stream()
                .map(TimeRoomCandidate::getTimeId)
                .collect(Collectors.toList())
                .stream()
                .distinct()
                .collect(Collectors.toList());
        List<String> unmatchedTimeIdList = timeIdList.stream()
                .filter(timeId -> destinationTimeRoomList.stream()
                        .noneMatch(timeRoom -> timeId.equals(timeRoom.getTimeId())))
                .collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(unmatchedTimeIdList)) {
            // 编排结果将要修改到的目标考场暂无对应批次
            String timeNames = iTimeService.lambdaQuery()
                    .select(Time::getName)
                    .eq(Time::getTaskId, arrangedEditParam.getTaskId())
                    .in(Time::getId, unmatchedTimeIdList)
                    .orderByAsc(Time::getCode)
                    .list().stream()
                    .map(Time::getName)
                    .collect(Collectors.joining(","));
            throw new KqException(ResponseCode.TIME_NOT_MATCH_FOR_THE_DESTINATION_ROOM.getCode(),
                    "编排结果将要修改到的目标考场暂无对应批次", "修改编排结果的目标考场中暂无以下批次: " + timeNames);
        }

        // 校验迁移后的编排数据中，是否存在同个考生考相同场次的情况
        if (CollectionUtils.isNotEmpty(destinationList)) {
            // 将迁移到目标考场已有的考生编排数据根据批次来分组，String是批次id、List是编排数据
            Map<String, List<TimeRoomCandidate>> destinationMap = destinationList.stream()
                    .collect(Collectors.groupingBy(TimeRoomCandidate::getTimeId));
            // 将要迁移考场的考生放入将要迁移的场次中
            beingEditList.forEach(beingTransferred -> destinationMap.forEach((desTimeId, desList) -> {
                if (beingTransferred.getTimeId().equals(desTimeId)) {
                    desList.add(beingTransferred);
                }
            }));
            // 校验迁移后的编排数据中，是否存在同个考生考相同场次的情况，String为批次id、List<String>为考生id的List
            Map<String, List<String>> repeatCandidateMap = new HashMap<>();
            destinationMap.forEach((desTimeId, desList) -> {
                // 过滤出每个批次中重复的考生
                List<String> repeatCandidateIdList = desList.stream()
                        .collect(Collectors.groupingBy(TimeRoomCandidate::getCandidateId, Collectors.counting()))
                        .entrySet().stream()
                        .filter(e -> e.getValue() > 1)
                        .map(Map.Entry::getKey)
                        .collect(Collectors.toList());
                // 将重复每个批次重复的考生id放入重复考生Map中
                if (CollectionUtils.isNotEmpty(repeatCandidateIdList)) {
                    repeatCandidateMap.put(desTimeId, repeatCandidateIdList);
                }
            });
            if (CollectionUtils.isNotEmpty(repeatCandidateMap)) {
                // 转换重复考生数据，返回给前端
                List<RepeatTimeCandidateVO> repeatTimeCandidateVOList = repeatCandidateMap.entrySet().stream()
                        .map(entry -> {
                            // 获取批次名称
                            String timeName = iTimeService.lambdaQuery()
                                    .select(Time::getName)
                                    .eq(Time::getTaskId, arrangedEditParam.getTaskId())
                                    .eq(Time::getId, entry.getKey())
                                    .last("LIMIT 1")
                                    .one().getName();
                            // 获取考生信息
                            List<String> candidateInfoList = iCandidateService.lambdaQuery()
                                    .select(Candidate::getName,
                                            Candidate::getIdNumber)
                                    .eq(Candidate::getTaskId, arrangedEditParam.getTaskId())
                                    .in(Candidate::getId, entry.getValue())
                                    .list().stream()
                                    .map(candidate -> StringUtils.join(candidate.getName(), "(",
                                            candidate.getIdNumber(), ")"))
                                    .collect(Collectors.toList());
                            RepeatTimeCandidateVO repeatTimeCandidateVO = new RepeatTimeCandidateVO();
                            repeatTimeCandidateVO.setTimeName(timeName).setCandidateInfo(candidateInfoList);
                            return repeatTimeCandidateVO;
                        }).collect(Collectors.toList());
                // 修改编排结果后，存在重复考生的编排数据
                throw new KqException(ResponseCode.DUPLICATED_ARRANGED_DATA_IN_TIME_ROOM_AFTER_EDIT.getCode(),
                        "修改编排结果后，存在重复考生的编排数据", repeatTimeCandidateVOList);
            }
        }
    }

    /**
     * 组装迁移考场后的编排信息
     * 
     * @param beingTransferredList 所有需要迁移考场的考生编排数据
     * @param timeRoomList         迁移到目标考场的场次信息
     */
    private void convertTransferredData(List<TimeRoomCandidate> beingTransferredList,
            List<TimeRoom> timeRoomList) {
        beingTransferredList.forEach(timeRoomCandidate -> {
            // 获取原考生编排信息中批次对应的新场次信息
            TimeRoom timeRoom = timeRoomList.stream()
                    .filter(tr -> timeRoomCandidate.getTimeId().equals(tr.getTimeId()))
                    .findFirst()
                    .orElseThrow(() -> new KqException(ResponseCode.ARRANGED_EDIT_EXCEPTION.getCode(),
                            "获取原考生编排信息中批次对应的新场次信息取值失败"));
            // 组装新的编排信息
            timeRoomCandidate.setTimeRoomId(timeRoom.getId())
                    .setRoomId(timeRoom.getRoomId());
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer unifiedAdmissionNumber(UnifiedAdmissionNumberParamVO unifiedAdmissionNumberParamVO) {

        // 1.获取考生现有的准考证号
        List<IdCardAdmissionNumberDTO> IdCardAdmissionNumberDTOList = timeRoomCandidateMapper
                .getIdCardAdmissionNumber(unifiedAdmissionNumberParamVO.getSourceTaskId());
        if (IdCardAdmissionNumberDTOList == null || IdCardAdmissionNumberDTOList.isEmpty()) {
            throw new KqException(ResponseCode.ERROR_UNIFIED_ADMISSION_NUMBER_NO_SOURCE_DATA.getCode(),
                    ResponseCode.ERROR_UNIFIED_ADMISSION_NUMBER_NO_SOURCE_DATA.getMsg());
        }

        Map<String, String> sourceMap = new HashMap();
        IdCardAdmissionNumberDTOList.stream().forEach(idCardAdmissionNumberDTO -> {
            String key = StringUtils.join(idCardAdmissionNumberDTO.getIdNumber(), "_",
                    idCardAdmissionNumberDTO.getSubjectCode());
            sourceMap.put(key, idCardAdmissionNumberDTO.getAdmissionNumber());
        });

        // 2.清空考生的准考证号
        this.lambdaUpdate()
                .eq(TimeRoomCandidate::getTaskId, unifiedAdmissionNumberParamVO.getTargetTaskId())
                .eq(TimeRoomCandidate::getDelFlag, false)
                .set(TimeRoomCandidate::getAdmissionNum, null)
                .update();

        // 3.赋值考生的准考证号
        List<IdCardAdmissionNumberDTO> targetIdCardAdmissionNumberDTOList = timeRoomCandidateMapper
                .getIdCardAdmissionNumber(unifiedAdmissionNumberParamVO.getTargetTaskId());

        if (targetIdCardAdmissionNumberDTOList == null || targetIdCardAdmissionNumberDTOList.isEmpty()) {
            throw new KqException(ResponseCode.ERROR_UNIFIED_ADMISSION_NUMBER_NO_TARGET_DATA.getCode(),
                    ResponseCode.ERROR_UNIFIED_ADMISSION_NUMBER_NO_TARGET_DATA.getMsg());
        }

        List<TimeRoomCandidate> updateTimeRoomCandidateList = new ArrayList<>();
        targetIdCardAdmissionNumberDTOList.stream().forEach(targetIdCardAdmissionNumberDTO -> {
            String key = StringUtils.join(targetIdCardAdmissionNumberDTO.getIdNumber(), "_",
                    targetIdCardAdmissionNumberDTO.getSubjectCode());
            String admissionNumber = sourceMap.get(key);
            if (StringUtils.isNotBlank(admissionNumber)) {
                TimeRoomCandidate updateTimeRoomCandidate = new TimeRoomCandidate();
                updateTimeRoomCandidate
                        .setAdmissionNum(admissionNumber)
                        .setId(targetIdCardAdmissionNumberDTO.getId());
                updateTimeRoomCandidateList.add(updateTimeRoomCandidate);
            }
        });

        this.updateBatchById(updateTimeRoomCandidateList);

        // 4.检测还有多少考生没有准考证号，手动处理吧！
        Integer count = this.lambdaQuery()
                .eq(TimeRoomCandidate::getTaskId, unifiedAdmissionNumberParamVO.getTargetTaskId())
                .isNull(TimeRoomCandidate::getAdmissionNum)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .count();
        return count;
    }

    @Override
    public List<TimeRoomCandidate> listByTaskRoomId(String taskId, String roomId) {
        return this.lambdaQuery().eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getRoomId, roomId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .list();
    }

    @Override
    public List<TaskRoom> getRoomByTime(String taskId, String timeId, String deptId, String areaId, String searchText) {
        return timeRoomCandidateMapper.getRoomByTime(taskId, timeId, deptId, areaId, searchText);
    }

    @Override
    public boolean hasArranged(String taskId, String timeId) {
        return this.lambdaQuery().select(TimeRoomCandidate::getId)
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(StringUtils.isNotBlank(timeId), TimeRoomCandidate::getTimeId, timeId)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .last("LIMIT 1")
                .oneOpt()
                .isPresent();
    }

    @Override
    public boolean hasCandidateArranged(String taskId, List<String> candidateIdList) {
        return this.lambdaQuery().select(TimeRoomCandidate::getId)
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .in(CollectionUtils.isNotEmpty(candidateIdList), TimeRoomCandidate::getCandidateId, candidateIdList)
                .eq(TimeRoomCandidate::getDelFlag, false)
                .last("LIMIT 1")
                .oneOpt()
                .isPresent();
    }

    @Override
    public List<Time> getTimeBySubject(String taskId, String subjectId) {
        return timeRoomCandidateMapper.getTimeBySubject(taskId, subjectId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void manualArrange(ManualArrangeParam manualArrangeParam) {
        // 校验编排数据是否已经锁定，已锁定不允许操作
        iTaskService.isLockDataThrowException(manualArrangeParam.getTaskId());
        // 获取任务编排参数
        Map<String, String> arrangeParamMap = this.getTaskArrangeParam(manualArrangeParam);
        // 校验手动编排参数
        this.verifyManualArrangeParam(manualArrangeParam);
        // 生成手动编排数据
        TimeRoomCandidate timeRoomCandidate = this.createManualArrange(manualArrangeParam, arrangeParamMap);
        // 保存手动编排数据
        this.saveManualArrange(timeRoomCandidate);
    }

    /**
     * 获取任务编排参数
     * 
     * @param manualArrangeParam 手动编排参数
     * @return 任务编排参数
     */
    private Map<String, String> getTaskArrangeParam(ManualArrangeParam manualArrangeParam) {
        // 获取编排参数，并且格式化
        Map<String, String> arrangeParamMap = timeRoomParamService
                .getTimeRoomParamMap(null, manualArrangeParam.getTaskId());
        // 编排参数校验
        this.verifyArrangeParam(arrangeParamMap);
        return arrangeParamMap;
    }

    /**
     * 校验手动编排参数
     * 
     * @param manualArrangeParam 手动编排参数
     */
    private void verifyManualArrangeParam(ManualArrangeParam manualArrangeParam) {
        // 校验考生是否存在
        if (!iCandidateService.hasCandidate(manualArrangeParam.getTaskId(),
                manualArrangeParam.getCandidateId())) {
            // 将要编排的考生不存在
            throw new KqException(ResponseCode.MANUAL_ARRANGE_CANDIDATE_NOT_FOUNT.getCode(),
                    ResponseCode.MANUAL_ARRANGE_CANDIDATE_NOT_FOUNT.getMsg());
        }
        // 校验科目是否存在
        if (!iSubjectService.hasSubject(manualArrangeParam.getTaskId(),
                manualArrangeParam.getSubjectId())) {
            // 将要编排的科目不存在
            throw new KqException(ResponseCode.MANUAL_ARRANGE_SUBJECT_NOT_FOUNT.getCode(),
                    ResponseCode.MANUAL_ARRANGE_SUBJECT_NOT_FOUNT.getMsg());
        }
        // 校验批次是否存在
        if (!iTimeService.hasTime(manualArrangeParam.getTaskId(),
                manualArrangeParam.getTimeId())) {
            // 将要编排的批次不存在
            throw new KqException(ResponseCode.MANUAL_ARRANGE_TIME_NOT_FOUNT.getCode(),
                    ResponseCode.MANUAL_ARRANGE_TIME_NOT_FOUNT.getMsg());
        }
        // 校验考场是否存在
        if (!iTaskRoomService.hasRoom(manualArrangeParam.getTaskId(),
                manualArrangeParam.getRoomId())) {
            // 将要编排的考场不存在
            throw new KqException(ResponseCode.MANUAL_ARRANGE_ROOM_NOT_FOUNT.getCode(),
                    ResponseCode.MANUAL_ARRANGE_ROOM_NOT_FOUNT.getMsg());
        }
        // 校验考生与科目的绑定关系是否正确
        if (!iCandidateSubjectService.hasCandidateSubject(manualArrangeParam.getTaskId(),
                manualArrangeParam.getCandidateId(),
                manualArrangeParam.getSubjectId())) {
            // 将要编排的考生与科目关系不正确
            throw new KqException(ResponseCode.MANUAL_ARRANGE_CANDIDATE_SUBJECT_MAP_ERROR.getCode(),
                    ResponseCode.MANUAL_ARRANGE_CANDIDATE_SUBJECT_MAP_ERROR.getMsg());
        }
        // 校验批次与科目的绑定关系是否正确
        if (!iTimeSubjectService.hasTimeSubject(manualArrangeParam.getTaskId(),
                manualArrangeParam.getTimeId(),
                manualArrangeParam.getSubjectId())) {
            // 将要编排的批次与科目关系不正确
            throw new KqException(ResponseCode.MANUAL_ARRANGE_TIME_SUBJECT_MAP_ERROR.getCode(),
                    ResponseCode.MANUAL_ARRANGE_TIME_SUBJECT_MAP_ERROR.getMsg());
        }
        // 校验考场与批次的绑定关系是否正确
        if (!iTimeRoomService.hasTimeRoom(manualArrangeParam.getTaskId(),
                manualArrangeParam.getTimeId(),
                manualArrangeParam.getRoomId())) {
            // 将要编排的考场与批次关系不正确
            throw new KqException(ResponseCode.MANUAL_ARRANGE_TIME_ROOM_MAP_ERROR.getCode(),
                    ResponseCode.MANUAL_ARRANGE_TIME_ROOM_MAP_ERROR.getMsg());
        }
        // 校验考生是否已经编排至指定考场、批次、科目中
        if (this.candidateHasArranged(manualArrangeParam)) {
            // 将要编排的考生已被编排至指定考场、批次、科目中
            throw new KqException(ResponseCode.MANUAL_ARRANGE_CANDIDATE_ARRANGED.getCode(),
                    ResponseCode.MANUAL_ARRANGE_CANDIDATE_ARRANGED.getMsg());
        }
    }

    /**
     * 考生是否已经编排至指定考场、批次、科目中
     * 
     * @param manualArrangeParam 手动编排参数
     * @return true：考生已编排至指定考场、批次、科目中，false：考生未编排至指定考场、批次、科目中
     */
    private boolean candidateHasArranged(ManualArrangeParam manualArrangeParam) {
        return this.lambdaQuery().select(TimeRoomCandidate::getId)
                .eq(TimeRoomCandidate::getTaskId, manualArrangeParam.getTaskId())
                .eq(TimeRoomCandidate::getCandidateId, manualArrangeParam.getCandidateId())
                .eq(TimeRoomCandidate::getRoomId, manualArrangeParam.getRoomId())
                .eq(TimeRoomCandidate::getTimeId, manualArrangeParam.getTimeId())
                .eq(TimeRoomCandidate::getSubjectId, manualArrangeParam.getSubjectId())
                .eq(TimeRoomCandidate::getDelFlag, false)
                .last("LIMIT 1")
                .oneOpt()
                .isPresent();
    }

    /**
     * 生成手动编排数据
     * 
     * @param manualArrangeParam 手动编排参数
     * @param arrangeParamMap    任务编排参数
     * @return 手动编排数据
     */
    private TimeRoomCandidate createManualArrange(ManualArrangeParam manualArrangeParam,
            Map<String, String> arrangeParamMap) {
        // 获取场次映射关系
        TimeRoom timeRoom = iTimeRoomService.one(manualArrangeParam.getTaskId(),
                manualArrangeParam.getTimeId(),
                manualArrangeParam.getRoomId());
        // 获取考生科目映射关系
        CandidateSubject candidateSubject = iCandidateSubjectService.one(manualArrangeParam.getTaskId(),
                manualArrangeParam.getCandidateId(),
                manualArrangeParam.getSubjectId());
        // 获取手动编排准考证号
        String admissionNumber = this.getManualArrangeAdmissionNumber(manualArrangeParam.getTaskId(),
                manualArrangeParam.getCandidateId(), arrangeParamMap);
        // 设置考生编排数据
        TimeRoomCandidate timeRoomCandidate = new TimeRoomCandidate();
        BeanUtils.copyProperties(manualArrangeParam, timeRoomCandidate);
        timeRoomCandidate.setTimeRoomId(timeRoom.getId())
                .setCandidateSubjectId(candidateSubject.getId())
                .setAdmissionNum(admissionNumber);
        return timeRoomCandidate;
    }

    /**
     * 获取手动编排准考证号
     * 
     * @param taskId          考试任务id
     * @param arrangeParamMap 考试任务编排参数
     * @return 手动编排准考证号
     */
    private String getManualArrangeAdmissionNumber(String taskId, String candidateId,
            Map<String, String> arrangeParamMap) {
        // 获取准考证号生成方式
        Integer admissionType = new Integer(arrangeParamMap.get("admissionType"));
        if (ArrangeParamCodeEnum.ADMISSION_TYPE_CUSTOM.getCode().equals(admissionType)) {
            // 使用自定义准考证号编排
            // 获取手动编排考生对应的准考证号
            Optional<CustomArrange> optional = iCustomArrangeService.getByCandidateId(taskId, candidateId);
            return optional.map(CustomArrange::getAdmissionNumber)
                    // 当前考生暂无准考证号，请录入考生准考证号，或使用其他准考证号生成方式
                    .orElseThrow(() -> new KqException(ResponseCode.MANUAL_ARRANGE_ADMISSION_NUMBER_NOT_FOUND.getCode(),
                            ResponseCode.MANUAL_ARRANGE_ADMISSION_NUMBER_NOT_FOUND.getMsg()));
        } else {
            // 使用准考证号生成规则生成准考证号
            TaskPlace taskPlace = iTaskPlaceService.getOneTaskPlace(taskId);
            TaskPlaceVO taskPlaceVO = new TaskPlaceVO();
            taskPlaceVO.setCity(taskPlace.getCity())
                    .setProvince(taskPlace.getProvince())
                    .setDistrict(taskPlace.getDistrict());
            return this.getAdmissionNumber(taskId, arrangeParamMap, taskPlaceVO);
        }
    }

    /**
     * 保存手动编排数据
     * 
     * @param timeRoomCandidate 手动编排数据
     */
    private void saveManualArrange(TimeRoomCandidate timeRoomCandidate) {
        if (!save(timeRoomCandidate)) {
            // 保存手动编排数据失败
            log.error("保存手动编排数据失败");
            throw new KqException(ResponseCode.MANUAL_ARRANGE_EXCEPTION.getCode(),
                    ResponseCode.MANUAL_ARRANGE_EXCEPTION.getMsg());
        }
    }

    @Override
    public List<String> listAdmissionNumberByTaskId(String taskId) {
        return timeRoomCandidateMapper.listAdmissionNumberByTaskId(taskId);
    }

    @SneakyThrows
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addOrUpdateCandidate(String taskId, MultipartFile file) {

        // 1.解锁，仅仅是解锁
        iTaskService.updateDataLock(taskId, false);

        // 2.读取excel文件，考生科目必须是完整的，如果是多科，必须是多条数据
        TaskCandidateListener listener = iCandidateService.readExcel(file, taskId);
        List<ImportTaskCandidateSheet> noRepeatCandidateSheets = listener.getImportNoRepeatTaskCandidates();
        if (org.springframework.util.CollectionUtils.isEmpty(noRepeatCandidateSheets)) {
            return;
        }

        // 3.删除考生
        noRepeatCandidateSheets.stream().forEach(importTaskCandidateSheet -> {
            // 一个一个删除
            Candidate candidate = iCandidateService.lambdaQuery()
                    .eq(Candidate::getTaskId, importTaskCandidateSheet.getTaskId())
                    .eq(Candidate::getIdNumber, importTaskCandidateSheet.getIdNumber())
                    .eq(Candidate::getDelFlag, false)
                    .last("LIMIT 1")
                    .one();

            if (candidate != null) {
                timeRoomCandidateMapper.deleteCandidateAllInfo(candidate.getTaskId(), candidate.getId());
            }

        });

        // 4.调用导入考生接口
        iCandidateService.saveImportCandidate(listener, taskId);

        // 5.调用继续编排接口
        this.candidateArrange(null, taskId, false);

        // 6.锁定数据
        iTaskService.updateDataLock(taskId, true);

        // 7.考生推送数据，推送数据时，需要先删除考试系统中的考生数据
        noRepeatCandidateSheets.stream().forEach(importTaskCandidateSheet -> {
            // 一个一个删除
            Candidate candidate = iCandidateService.lambdaQuery()
                    .eq(Candidate::getTaskId, importTaskCandidateSheet.getTaskId())
                    .eq(Candidate::getIdNumber, importTaskCandidateSheet.getIdNumber())
                    .eq(Candidate::getDelFlag, false)
                    .last("LIMIT 1")
                    .one();

            this.pushSimpleCandidateToKs(taskId, candidate.getId(), StringCodeEnum.PUSH_TYPE_DELETE.getCode());
            this.pushSimpleCandidateToKs(taskId, candidate.getId(), StringCodeEnum.PUSH_TYPE_INSERT.getCode());

        });
    }

    @Override
    public CandidateAreaVO getCandidateAreaVO(String taskId, String timeId, String admissionNumber) {
        return timeRoomCandidateMapper.getCandidateArea(taskId, timeId, admissionNumber);
    }

    @Override
    public TimeRoomCandidate geTimeRoomCandidateByTaskidAndAdnumber(String taskId, String examId, String admissionNum) {
        return timeRoomCandidateMapper.geTimeRoomCandidateByTaskidAndAdnumber(taskId, examId, admissionNum);
    }

    /**
     * 通过任务id和准考证查询
     *
     * @param taskId
     * @param admissionNumber
     * @return
     */
    @Override
    public TimeRoomCandidate get(String taskId, String admissionNumber) {
        return lambdaQuery()
                .eq(BaseEntity::getDelFlag, false)
                .eq(TimeRoomCandidate::getTaskId, taskId)
                .eq(TimeRoomCandidate::getAdmissionNum, admissionNumber)
                .last("limit 1").one();
    }

    /**
     * 根据timeId统计编排人数
     * @param taskId
     * @return
     */
    @Override
    public List<ArrangeCountVo> countCandidateByTime(String taskId){
        return timeRoomCandidateMapper.countCandidateByTime(taskId);
    }

}
